@damurka/jovian 0.1.0 → 0.1.2
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 +7 -11
- package/docs/api/README.md +10 -1
- package/docs/api/types.md +2 -2
- package/docs/development.md +2 -2
- package/docs/getting-started.md +2 -2
- package/docs/releasing.md +32 -10
- package/docs/troubleshooting.md +9 -4
- package/lib/execution/execution-queue.js +1 -1
- package/lib/session/native-paths.d.ts +1 -1
- package/lib/session/native-paths.js +1 -1
- package/lib/session/r-setup.d.ts +47 -0
- package/lib/session/r-setup.js +238 -0
- package/lib/session/runtimes.d.ts +29 -0
- package/lib/session/runtimes.js +80 -0
- package/lib/session/session-manager.d.ts +17 -5
- package/lib/session/session-manager.js +34 -8
- package/lib/session/supervisor-client.d.ts +14 -3
- package/lib/session/supervisor-client.js +39 -6
- package/lib/types/engine.d.ts +62 -37
- package/lib/utils/logger.d.ts +14 -3
- package/lib/utils/logger.js +38 -30
- package/package.json +11 -7
- package/packages/hera/DESCRIPTION +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
const runCommand = (command, args) => {
|
|
3
|
+
try {
|
|
4
|
+
const output = execFileSync(command, args, {
|
|
5
|
+
encoding: 'utf8',
|
|
6
|
+
timeout: 10_000,
|
|
7
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
8
|
+
windowsHide: true
|
|
9
|
+
});
|
|
10
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
11
|
+
return lines[lines.length - 1];
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const defaultContext = () => ({ run: runCommand, env: process.env, platform: process.platform });
|
|
18
|
+
/**
|
|
19
|
+
* Where R lives, when the caller did not say: $R_HOME, else what `R RHOME`
|
|
20
|
+
* prints (R's own answer, valid on every platform, if R is on PATH), else on
|
|
21
|
+
* Windows the install path R's installer records in the registry.
|
|
22
|
+
*/
|
|
23
|
+
export function discoverRHome(context = defaultContext()) {
|
|
24
|
+
if (context.env.R_HOME)
|
|
25
|
+
return context.env.R_HOME;
|
|
26
|
+
const fromR = context.run('R', ['RHOME']);
|
|
27
|
+
if (fromR)
|
|
28
|
+
return fromR;
|
|
29
|
+
if (context.platform === 'win32') {
|
|
30
|
+
for (const hive of ['HKLM', 'HKCU']) {
|
|
31
|
+
const line = context.run('reg', ['query', `${hive}\\SOFTWARE\\R-core\\R`, '/v', 'InstallPath']);
|
|
32
|
+
const match = line?.match(/InstallPath\s+REG_SZ\s+(.+)$/);
|
|
33
|
+
if (match?.[1])
|
|
34
|
+
return match[1].trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Which Python to embed, when the caller did not say: $PYTHONHOME, else the
|
|
41
|
+
* installation prefix of the first python on PATH. sys.base_prefix, not
|
|
42
|
+
* sys.prefix: inside a virtual environment the latter is the venv, which has
|
|
43
|
+
* no libpython to load.
|
|
44
|
+
*/
|
|
45
|
+
export function discoverPythonHome(context = defaultContext()) {
|
|
46
|
+
if (context.env.PYTHONHOME)
|
|
47
|
+
return context.env.PYTHONHOME;
|
|
48
|
+
const script = 'import sys; print(sys.base_prefix)';
|
|
49
|
+
const candidates = [
|
|
50
|
+
['python3', ['-c', script]],
|
|
51
|
+
['python', ['-c', script]]
|
|
52
|
+
];
|
|
53
|
+
if (context.platform === 'win32')
|
|
54
|
+
candidates.push(['py', ['-3', '-c', script]]);
|
|
55
|
+
for (const [command, args] of candidates) {
|
|
56
|
+
const home = context.run(command, args);
|
|
57
|
+
if (home)
|
|
58
|
+
return home;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The options with rHome (R sessions) or pythonHome (Python sessions) filled
|
|
64
|
+
* in when the caller left them out and the runtime could be found. Anything
|
|
65
|
+
* the caller passed is kept as it is; if nothing is found the field stays
|
|
66
|
+
* unset, and the kernel reports what it could not find.
|
|
67
|
+
*/
|
|
68
|
+
export function withDiscoveredRuntime(options, context = defaultContext()) {
|
|
69
|
+
if (options.kernelType === 'python') {
|
|
70
|
+
if (options.pythonHome)
|
|
71
|
+
return options;
|
|
72
|
+
const pythonHome = discoverPythonHome(context);
|
|
73
|
+
return pythonHome ? { ...options, pythonHome } : options;
|
|
74
|
+
}
|
|
75
|
+
if (options.rHome)
|
|
76
|
+
return options;
|
|
77
|
+
const rHome = discoverRHome(context);
|
|
78
|
+
return rHome ? { ...options, rHome } : options;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=runtimes.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
-
import type { CommInfoReplyContent, CompleteReplyContent, EngineOptions, ExecutionHistoryEntry, ExecutionOptions, ExecutionResult, InspectReplyContent, IsCompleteReplyContent, KernelHistoryEntry, KernelHistoryOptions, KernelInfoReplyContent, SessionStatusInfo, ShinyAppHandle, ShinyAppOptions } from '../types/index.js';
|
|
2
|
+
import type { CommInfoReplyContent, CompleteReplyContent, EngineOptions, ExecutionHistoryEntry, ExecutionOptions, ExecutionResult, InspectReplyContent, IsCompleteReplyContent, KernelHistoryEntry, KernelHistoryOptions, KernelInfoReplyContent, LoggerFunction, LogThreshold, SessionManagerOptions, SessionStatusInfo, ShinyAppHandle, ShinyAppOptions } from '../types/index.js';
|
|
3
3
|
import type { ExecutionState } from '../types/messages.js';
|
|
4
4
|
import { SupervisorClient, type SessionConnectionInfo } from './supervisor-client.js';
|
|
5
5
|
import { Comm } from './comm.js';
|
|
@@ -27,7 +27,10 @@ export declare class Session extends EventEmitter {
|
|
|
27
27
|
private readonly executionHistory;
|
|
28
28
|
private readonly executionHistoryByMsgId;
|
|
29
29
|
private readonly historyStreamChars;
|
|
30
|
-
constructor(info: SessionConnectionInfo, options: EngineOptions, supervisor: SupervisorClient
|
|
30
|
+
constructor(info: SessionConnectionInfo, options: EngineOptions, supervisor: SupervisorClient, logging?: {
|
|
31
|
+
level?: LogThreshold | undefined;
|
|
32
|
+
logger?: LoggerFunction | undefined;
|
|
33
|
+
});
|
|
31
34
|
/**
|
|
32
35
|
* (Re)establishes the WebSocket to this.info's session and resolves
|
|
33
36
|
* once it's ready. Used both by the constructor and by restart() --
|
|
@@ -122,7 +125,7 @@ export declare class Session extends EventEmitter {
|
|
|
122
125
|
* execute() timed out) first.
|
|
123
126
|
*/
|
|
124
127
|
interrupt(options?: {
|
|
125
|
-
timeout?: number;
|
|
128
|
+
timeout?: number | undefined;
|
|
126
129
|
}): Promise<boolean>;
|
|
127
130
|
/**
|
|
128
131
|
* The latest iopub `status` the kernel reported ('busy' while it is
|
|
@@ -145,7 +148,7 @@ export declare class Session extends EventEmitter {
|
|
|
145
148
|
* shutdown_request (stop()/restart()) -- the supervisor rejects those.
|
|
146
149
|
*/
|
|
147
150
|
request<T = any>(msgType: string, content?: Record<string, unknown>, options?: {
|
|
148
|
-
timeout?: number;
|
|
151
|
+
timeout?: number | undefined;
|
|
149
152
|
}): Promise<T>;
|
|
150
153
|
private trackExecutionState;
|
|
151
154
|
private routeComm;
|
|
@@ -209,11 +212,20 @@ export declare class Session extends EventEmitter {
|
|
|
209
212
|
kill(): void;
|
|
210
213
|
}
|
|
211
214
|
export declare class SessionManager {
|
|
215
|
+
private readonly logLevel;
|
|
216
|
+
private readonly customLogger;
|
|
217
|
+
private readonly logger;
|
|
212
218
|
private readonly supervisor;
|
|
213
219
|
private readonly sessions;
|
|
214
220
|
private exitHandlerRegistered;
|
|
221
|
+
/**
|
|
222
|
+
* Quiet by default: only one-time setup notices, warnings and errors are
|
|
223
|
+
* printed. See SessionManagerOptions for `logLevel`, `logger` and
|
|
224
|
+
* `kernelOutput` (and the JOVIAN_LOG_LEVEL / JOVIAN_KERNEL_OUTPUT variables).
|
|
225
|
+
*/
|
|
226
|
+
constructor(options?: SessionManagerOptions);
|
|
215
227
|
/** Creates a new R session in its own OS process and waits for it to be ready. */
|
|
216
|
-
createSession(
|
|
228
|
+
createSession(requested?: EngineOptions): Promise<Session>;
|
|
217
229
|
/** Gracefully stops every session managed by this instance. */
|
|
218
230
|
stopAll(): Promise<void>;
|
|
219
231
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
|
-
import { Logger } from '../utils/logger.js';
|
|
3
|
+
import { Logger, defaultLogLevel } from '../utils/logger.js';
|
|
4
4
|
import { MessageRouter } from '../messaging/message-router.js';
|
|
5
5
|
import { ExecutionQueue } from '../execution/execution-queue.js';
|
|
6
6
|
import { MiddlewareChain } from '../middleware/middleware-chain.js';
|
|
@@ -12,6 +12,9 @@ import { ErrorHandler } from '../handlers/error-handler.js';
|
|
|
12
12
|
import { DisplayHandler } from '../handlers/display-handler.js';
|
|
13
13
|
import { findFreePort, waitForPort } from '../utils/network.js';
|
|
14
14
|
import { SupervisorClient } from './supervisor-client.js';
|
|
15
|
+
import { withDiscoveredRuntime } from './runtimes.js';
|
|
16
|
+
import { ensureRPackages } from './r-setup.js';
|
|
17
|
+
import { bundledHeraSource } from './native-paths.js';
|
|
15
18
|
import { Comm } from './comm.js';
|
|
16
19
|
const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
|
|
17
20
|
// How long stop() waits for the kernel's shutdown_reply after the
|
|
@@ -86,12 +89,12 @@ export class Session extends EventEmitter {
|
|
|
86
89
|
executionHistory = [];
|
|
87
90
|
executionHistoryByMsgId = new Map();
|
|
88
91
|
historyStreamChars = new WeakMap();
|
|
89
|
-
constructor(info, options, supervisor) {
|
|
92
|
+
constructor(info, options, supervisor, logging = {}) {
|
|
90
93
|
super();
|
|
91
94
|
this.info = info;
|
|
92
95
|
this.currentOptions = options;
|
|
93
96
|
this.supervisor = supervisor;
|
|
94
|
-
this.logger = new Logger(options.logger);
|
|
97
|
+
this.logger = new Logger(options.logger ?? logging.logger, logging.level);
|
|
95
98
|
this.on('message', (message) => {
|
|
96
99
|
this.recordExecutionHistory(message);
|
|
97
100
|
this.settleRequest(message);
|
|
@@ -213,7 +216,7 @@ export class Session extends EventEmitter {
|
|
|
213
216
|
// The supervisor replaces a session's options wholesale, so send the
|
|
214
217
|
// merge -- a restart that only switches rHome must keep the
|
|
215
218
|
// workingDirectory, rLibs, ... the session was created with.
|
|
216
|
-
const mergedOptions = options ? { ...this.currentOptions, ...options } : undefined;
|
|
219
|
+
const mergedOptions = options ? withDiscoveredRuntime({ ...this.currentOptions, ...options }) : undefined;
|
|
217
220
|
// Reassigned synchronously, before awaiting anything below, so a
|
|
218
221
|
// concurrent execute()/createShiny() call that reads this.readyPromise
|
|
219
222
|
// while the restart is still in flight waits for the new connection
|
|
@@ -222,6 +225,8 @@ export class Session extends EventEmitter {
|
|
|
222
225
|
const shutdownReply = this.watchFor('shutdown_reply');
|
|
223
226
|
this.readyPromise = (async () => {
|
|
224
227
|
try {
|
|
228
|
+
if (mergedOptions)
|
|
229
|
+
await ensureRPackages(mergedOptions, this.logger);
|
|
225
230
|
await this.supervisor.restartSession(this.info, mergedOptions);
|
|
226
231
|
if (mergedOptions) {
|
|
227
232
|
this.currentOptions = mergedOptions;
|
|
@@ -776,13 +781,34 @@ export class Session extends EventEmitter {
|
|
|
776
781
|
}
|
|
777
782
|
}
|
|
778
783
|
export class SessionManager {
|
|
779
|
-
|
|
784
|
+
logLevel;
|
|
785
|
+
customLogger;
|
|
786
|
+
logger;
|
|
787
|
+
supervisor;
|
|
780
788
|
sessions = new Set();
|
|
781
789
|
exitHandlerRegistered = false;
|
|
790
|
+
/**
|
|
791
|
+
* Quiet by default: only one-time setup notices, warnings and errors are
|
|
792
|
+
* printed. See SessionManagerOptions for `logLevel`, `logger` and
|
|
793
|
+
* `kernelOutput` (and the JOVIAN_LOG_LEVEL / JOVIAN_KERNEL_OUTPUT variables).
|
|
794
|
+
*/
|
|
795
|
+
constructor(options = {}) {
|
|
796
|
+
this.logLevel = options.logLevel ?? defaultLogLevel();
|
|
797
|
+
this.customLogger = options.logger;
|
|
798
|
+
this.logger = new Logger(this.customLogger, this.logLevel);
|
|
799
|
+
const verbose = this.logLevel === 'trace' || this.logLevel === 'debug';
|
|
800
|
+
const forwardKernelOutput = options.kernelOutput ?? (Boolean(process.env.JOVIAN_KERNEL_OUTPUT) || verbose);
|
|
801
|
+
this.supervisor = new SupervisorClient(this.logger, { forwardKernelOutput });
|
|
802
|
+
}
|
|
782
803
|
/** Creates a new R session in its own OS process and waits for it to be ready. */
|
|
783
|
-
async createSession(
|
|
804
|
+
async createSession(requested = {}) {
|
|
805
|
+
// Finds R / Python when rHome / pythonHome were not given (see runtimes.ts).
|
|
806
|
+
// An installed package brings its own copy of hera (none in a source checkout).
|
|
807
|
+
const options = withDiscoveredRuntime(requested.heraSrcPath ? requested : { ...requested, heraSrcPath: bundledHeraSource() });
|
|
808
|
+
// First R session only: installs hera and what it needs (see r-setup.ts).
|
|
809
|
+
await ensureRPackages(options, this.logger);
|
|
784
810
|
const info = await this.supervisor.createSession(options);
|
|
785
|
-
const session = new Session(info, options, this.supervisor);
|
|
811
|
+
const session = new Session(info, options, this.supervisor, { level: this.logLevel, logger: this.customLogger });
|
|
786
812
|
this.sessions.add(session);
|
|
787
813
|
this.registerExitHandler();
|
|
788
814
|
try {
|
|
@@ -798,7 +824,7 @@ export class SessionManager {
|
|
|
798
824
|
async stopAll() {
|
|
799
825
|
await Promise.all([...this.sessions].map((session) => session.stop()));
|
|
800
826
|
this.sessions.clear();
|
|
801
|
-
this.supervisor.kill();
|
|
827
|
+
this.supervisor.kill(true);
|
|
802
828
|
}
|
|
803
829
|
/**
|
|
804
830
|
* Forcibly terminates every session. Prefer stopAll(), but a session
|
|
@@ -10,7 +10,14 @@ export declare class SupervisorClient {
|
|
|
10
10
|
private child;
|
|
11
11
|
private readyPromise;
|
|
12
12
|
private readonly logger;
|
|
13
|
-
|
|
13
|
+
private readonly forwardKernelOutput;
|
|
14
|
+
private readonly recentOutput;
|
|
15
|
+
constructor(logger: Logger, options?: {
|
|
16
|
+
forwardKernelOutput?: boolean;
|
|
17
|
+
});
|
|
18
|
+
private rememberOutput;
|
|
19
|
+
/** The message plus what the kernels said just before, when that was not already printed. */
|
|
20
|
+
private withKernelOutput;
|
|
14
21
|
private ensureStarted;
|
|
15
22
|
private spawnSupervisor;
|
|
16
23
|
createSession(options: EngineOptions): Promise<SessionConnectionInfo>;
|
|
@@ -30,7 +37,11 @@ export declare class SupervisorClient {
|
|
|
30
37
|
* close this session and create a new one just to pick a different R.
|
|
31
38
|
*/
|
|
32
39
|
restartSession(info: SessionConnectionInfo, options?: Partial<EngineOptions>): Promise<void>;
|
|
33
|
-
/**
|
|
34
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Skips graceful per-session shutdown -- only for cleanup on the way out.
|
|
42
|
+
* `expected` is the normal end of stopAll(), after every session was
|
|
43
|
+
* stopped: not worth a warning.
|
|
44
|
+
*/
|
|
45
|
+
kill(expected?: boolean): void;
|
|
35
46
|
}
|
|
36
47
|
//# sourceMappingURL=supervisor-client.d.ts.map
|
|
@@ -33,8 +33,29 @@ export class SupervisorClient {
|
|
|
33
33
|
child;
|
|
34
34
|
readyPromise;
|
|
35
35
|
logger;
|
|
36
|
-
|
|
36
|
+
forwardKernelOutput;
|
|
37
|
+
// The supervisor's stderr is where every kernel's start-up output ends up
|
|
38
|
+
// ([elara] ..., [carpo] ...). It is kept, not printed, unless asked for,
|
|
39
|
+
// and attached to the error when a kernel fails to start.
|
|
40
|
+
recentOutput = [];
|
|
41
|
+
constructor(logger, options = {}) {
|
|
37
42
|
this.logger = logger;
|
|
43
|
+
this.forwardKernelOutput = options.forwardKernelOutput ?? false;
|
|
44
|
+
}
|
|
45
|
+
rememberOutput(line) {
|
|
46
|
+
if (!line.trim())
|
|
47
|
+
return;
|
|
48
|
+
this.recentOutput.push(line);
|
|
49
|
+
if (this.recentOutput.length > 200)
|
|
50
|
+
this.recentOutput.shift();
|
|
51
|
+
}
|
|
52
|
+
/** The message plus what the kernels said just before, when that was not already printed. */
|
|
53
|
+
withKernelOutput(message) {
|
|
54
|
+
if (this.forwardKernelOutput || this.recentOutput.length === 0)
|
|
55
|
+
return message;
|
|
56
|
+
const notable = this.recentOutput.filter((line) => /error|fatal|warning|failed|cannot|not found|no such/i.test(line));
|
|
57
|
+
const lines = (notable.length > 0 ? notable : this.recentOutput).slice(-8);
|
|
58
|
+
return `${message}\nKernel output:\n ${lines.join('\n ')}`;
|
|
38
59
|
}
|
|
39
60
|
ensureStarted() {
|
|
40
61
|
if (!this.readyPromise) {
|
|
@@ -48,7 +69,11 @@ export class SupervisorClient {
|
|
|
48
69
|
this.logger.debug(`Spawning supervisor process from: ${exePath}`);
|
|
49
70
|
const child = spawn(exePath, [], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
50
71
|
this.child = child;
|
|
51
|
-
child.stderr
|
|
72
|
+
createInterface({ input: child.stderr }).on('line', (line) => {
|
|
73
|
+
this.rememberOutput(line);
|
|
74
|
+
if (this.forwardKernelOutput)
|
|
75
|
+
process.stderr.write(`${line}\n`);
|
|
76
|
+
});
|
|
52
77
|
const rl = createInterface({ input: child.stdout });
|
|
53
78
|
const onLine = (line) => {
|
|
54
79
|
let message;
|
|
@@ -87,7 +112,7 @@ export class SupervisorClient {
|
|
|
87
112
|
});
|
|
88
113
|
const body = await res.json();
|
|
89
114
|
if (!res.ok || !body.sessionId) {
|
|
90
|
-
throw new Error(body.error ?? `Supervisor failed to create session (HTTP ${res.status})`);
|
|
115
|
+
throw new Error(this.withKernelOutput(body.error ?? `Supervisor failed to create session (HTTP ${res.status})`));
|
|
91
116
|
}
|
|
92
117
|
return { sessionId: body.sessionId, httpBase, wsBase: `ws://127.0.0.1:${wsPort}` };
|
|
93
118
|
}
|
|
@@ -126,10 +151,18 @@ export class SupervisorClient {
|
|
|
126
151
|
throw new Error(body.error ?? `Supervisor failed to restart session ${info.sessionId} (HTTP ${res.status})`);
|
|
127
152
|
}
|
|
128
153
|
}
|
|
129
|
-
/**
|
|
130
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Skips graceful per-session shutdown -- only for cleanup on the way out.
|
|
156
|
+
* `expected` is the normal end of stopAll(), after every session was
|
|
157
|
+
* stopped: not worth a warning.
|
|
158
|
+
*/
|
|
159
|
+
kill(expected = false) {
|
|
131
160
|
if (this.child && !this.child.killed) {
|
|
132
|
-
|
|
161
|
+
const message = `Force-killing supervisor process (pid ${this.child.pid})`;
|
|
162
|
+
if (expected)
|
|
163
|
+
this.logger.debug(message);
|
|
164
|
+
else
|
|
165
|
+
this.logger.warn(message);
|
|
133
166
|
this.child.kill();
|
|
134
167
|
}
|
|
135
168
|
}
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import type { JupyterMessage } from './messages.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* 'notice' is for the few things a user should be told even when the library
|
|
4
|
+
* is otherwise quiet (e.g. the one-time install of the R packages, which takes
|
|
5
|
+
* a while); it sits between 'info' and 'warn'.
|
|
6
|
+
*/
|
|
7
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'notice' | 'warn' | 'error';
|
|
8
|
+
/** What the built-in console logger prints: this level and above; 'silent' prints nothing. */
|
|
9
|
+
export type LogThreshold = LogLevel | 'silent';
|
|
10
|
+
export interface SessionManagerOptions {
|
|
11
|
+
/**
|
|
12
|
+
* How much the library prints to the console (default 'notice': one-time
|
|
13
|
+
* setup messages, warnings and errors; env JOVIAN_LOG_LEVEL sets the
|
|
14
|
+
* default). Ignored for messages sent to `logger`, which receives all of them.
|
|
15
|
+
*/
|
|
16
|
+
logLevel?: LogThreshold | undefined;
|
|
17
|
+
/** Receives every log message instead of the console. */
|
|
18
|
+
logger?: LoggerFunction | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Print the kernels' own start-up output (the `[elara]` / `[carpo]` lines)
|
|
21
|
+
* to stderr as it happens. Off by default -- it is included in the error
|
|
22
|
+
* when a kernel fails to start -- and on when logLevel is 'debug' or
|
|
23
|
+
* 'trace' or env JOVIAN_KERNEL_OUTPUT is set.
|
|
24
|
+
*/
|
|
25
|
+
kernelOutput?: boolean | undefined;
|
|
26
|
+
}
|
|
3
27
|
export type LoggerFunction = (level: LogLevel, message: string, data?: any) => void;
|
|
4
28
|
export interface EngineOptions {
|
|
5
29
|
/**
|
|
@@ -9,12 +33,13 @@ export interface EngineOptions {
|
|
|
9
33
|
* supervisor actually uses (native/src/themisto/session_registry.cpp's
|
|
10
34
|
* SessionOptions::kernelType) and which kernel executable it spawns.
|
|
11
35
|
*/
|
|
12
|
-
kernelType?: 'r' | 'python';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
kernelType?: 'r' | 'python' | undefined;
|
|
37
|
+
/** R installation (`R RHOME`). Found from $R_HOME, `R RHOME` or the Windows registry when omitted. */
|
|
38
|
+
rHome?: string | undefined;
|
|
39
|
+
rPath?: string | undefined;
|
|
40
|
+
rLibs?: string | undefined;
|
|
16
41
|
/** Directory containing the pandoc binary, for bundled R installs that don't ship it on PATH. */
|
|
17
|
-
pandocPath?: string;
|
|
42
|
+
pandocPath?: string | undefined;
|
|
18
43
|
/**
|
|
19
44
|
* Source directory of the 'hera' R package (packages/hera in this repo).
|
|
20
45
|
* When set, Elara installs it via remotes::install_local() if it is
|
|
@@ -23,13 +48,13 @@ export interface EngineOptions {
|
|
|
23
48
|
* already installed in the library -- install or update it with
|
|
24
49
|
* `npm run hera:install`.
|
|
25
50
|
*/
|
|
26
|
-
heraSrcPath?: string;
|
|
27
|
-
/** Only used when kernelType is 'python' -- Carpo's equivalent of rHome. */
|
|
28
|
-
pythonHome?: string;
|
|
51
|
+
heraSrcPath?: string | undefined;
|
|
52
|
+
/** Only used when kernelType is 'python' -- Carpo's equivalent of rHome. Found from $PYTHONHOME or `python3`/`python` on PATH when omitted. */
|
|
53
|
+
pythonHome?: string | undefined;
|
|
29
54
|
/** Only used when kernelType is 'python' -- Carpo's equivalent of rPath. */
|
|
30
|
-
pythonPath?: string;
|
|
55
|
+
pythonPath?: string | undefined;
|
|
31
56
|
/** Only used when kernelType is 'python' -- not yet consulted by Carpo itself (see carpo::EnvironmentConfig). */
|
|
32
|
-
venvPath?: string;
|
|
57
|
+
venvPath?: string | undefined;
|
|
33
58
|
/**
|
|
34
59
|
* Directory the kernel process starts in -- what `getwd()` (R) /
|
|
35
60
|
* `os.getcwd()` (Python) report and what relative paths resolve against.
|
|
@@ -37,44 +62,44 @@ export interface EngineOptions {
|
|
|
37
62
|
* (i.e. the calling process's), which is rarely what you want for a
|
|
38
63
|
* notebook/project: set it to the project or document folder.
|
|
39
64
|
*/
|
|
40
|
-
workingDirectory?: string;
|
|
41
|
-
queueSize?: number;
|
|
42
|
-
enableLogging?: boolean;
|
|
43
|
-
enableMetrics?: boolean;
|
|
44
|
-
logger?: LoggerFunction;
|
|
65
|
+
workingDirectory?: string | undefined;
|
|
66
|
+
queueSize?: number | undefined;
|
|
67
|
+
enableLogging?: boolean | undefined;
|
|
68
|
+
enableMetrics?: boolean | undefined;
|
|
69
|
+
logger?: LoggerFunction | undefined;
|
|
45
70
|
}
|
|
46
71
|
export type EngineState = 'idle' | 'starting' | 'running' | 'stopping' | 'stopped' | 'error';
|
|
47
72
|
export interface ExecutionOptions {
|
|
48
|
-
silent?: boolean;
|
|
49
|
-
storeHistory?: boolean;
|
|
50
|
-
allowStdin?: boolean;
|
|
73
|
+
silent?: boolean | undefined;
|
|
74
|
+
storeHistory?: boolean | undefined;
|
|
75
|
+
allowStdin?: boolean | undefined;
|
|
51
76
|
/**
|
|
52
77
|
* When this execution fails, abort every execute() still waiting behind
|
|
53
78
|
* it in the queue instead of running them (Jupyter's stop_on_error) --
|
|
54
79
|
* their results come back with `aborted: true` and nothing having run.
|
|
55
80
|
* Also forwarded to the kernel in the execute_request itself.
|
|
56
81
|
*/
|
|
57
|
-
stopOnError?: boolean;
|
|
82
|
+
stopOnError?: boolean | undefined;
|
|
58
83
|
/**
|
|
59
84
|
* Expressions to evaluate in the kernel right after the code runs, as
|
|
60
85
|
* {name: expression} (Jupyter's user_expressions). Only evaluated when
|
|
61
86
|
* the code succeeded; each result -- or its own error -- comes back in
|
|
62
87
|
* `ExecutionResult.userExpressions` under the same name.
|
|
63
88
|
*/
|
|
64
|
-
userExpressions?: Record<string, string
|
|
89
|
+
userExpressions?: Record<string, string> | undefined;
|
|
65
90
|
/**
|
|
66
91
|
* Milliseconds to wait for the execution to finish before giving up
|
|
67
92
|
* (default 30000; 0 = no timeout, for calls meant to run indefinitely
|
|
68
93
|
* such as a Shiny app).
|
|
69
94
|
*/
|
|
70
|
-
timeout?: number;
|
|
95
|
+
timeout?: number | undefined;
|
|
71
96
|
/**
|
|
72
97
|
* When the timeout fires, also send the kernel an interrupt (default
|
|
73
98
|
* true) so it stops the code instead of carrying on with work no one is
|
|
74
99
|
* waiting for -- which would otherwise block everything queued behind it.
|
|
75
100
|
* Set false to leave the kernel running after a timeout.
|
|
76
101
|
*/
|
|
77
|
-
interruptOnTimeout?: boolean;
|
|
102
|
+
interruptOnTimeout?: boolean | undefined;
|
|
78
103
|
}
|
|
79
104
|
/** One evaluated user expression: its rich value, or the error evaluating it raised. */
|
|
80
105
|
export type UserExpressionResult = {
|
|
@@ -131,19 +156,19 @@ export interface ExecutionHistoryEntry {
|
|
|
131
156
|
*/
|
|
132
157
|
export interface KernelHistoryOptions {
|
|
133
158
|
/** Defaults to 'tail' -- the n most recent executions. */
|
|
134
|
-
histAccessType?: 'tail' | 'range' | 'search';
|
|
159
|
+
histAccessType?: 'tail' | 'range' | 'search' | undefined;
|
|
135
160
|
/** 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;
|
|
161
|
+
output?: boolean | undefined;
|
|
162
|
+
raw?: boolean | undefined;
|
|
138
163
|
/** Max entries to return ('tail'/'search'). Defaults to 100. */
|
|
139
|
-
n?: number;
|
|
164
|
+
n?: number | undefined;
|
|
140
165
|
/** 'range' only. */
|
|
141
|
-
session?: number;
|
|
142
|
-
start?: number;
|
|
143
|
-
stop?: number;
|
|
166
|
+
session?: number | undefined;
|
|
167
|
+
start?: number | undefined;
|
|
168
|
+
stop?: number | undefined;
|
|
144
169
|
/** 'search' only -- a glob pattern (*, ?). */
|
|
145
|
-
pattern?: string;
|
|
146
|
-
unique?: boolean;
|
|
170
|
+
pattern?: string | undefined;
|
|
171
|
+
unique?: boolean | undefined;
|
|
147
172
|
}
|
|
148
173
|
/**
|
|
149
174
|
* One entry from a real history_reply: [session, line_number, input], or
|
|
@@ -179,13 +204,13 @@ export interface ShinyAppOptions {
|
|
|
179
204
|
/** Directory containing the Shiny app (server.R/ui.R or app.R). */
|
|
180
205
|
appDir: string;
|
|
181
206
|
/** Defaults to an OS-assigned free port. */
|
|
182
|
-
port?: number;
|
|
207
|
+
port?: number | undefined;
|
|
183
208
|
/** Defaults to '127.0.0.1'. */
|
|
184
|
-
host?: string;
|
|
209
|
+
host?: string | undefined;
|
|
185
210
|
/** Defaults to false -- the caller decides how/where to display the app. */
|
|
186
|
-
launchBrowser?: boolean;
|
|
211
|
+
launchBrowser?: boolean | undefined;
|
|
187
212
|
/** Max time to wait for the app to start accepting connections, in ms. Defaults to 10000. */
|
|
188
|
-
readyTimeout?: number;
|
|
213
|
+
readyTimeout?: number | undefined;
|
|
189
214
|
/**
|
|
190
215
|
* Environment variables to set (via Sys.setenv()) in the R session
|
|
191
216
|
* before launching the app -- e.g. rmncah's app.R reads
|
|
@@ -195,7 +220,7 @@ export interface ShinyAppOptions {
|
|
|
195
220
|
* that reads them after runApp() starts, since Sys.setenv() itself runs
|
|
196
221
|
* synchronously right before it in the same execute() call.
|
|
197
222
|
*/
|
|
198
|
-
env?: Record<string, string
|
|
223
|
+
env?: Record<string, string> | undefined;
|
|
199
224
|
}
|
|
200
225
|
export interface ShinyAppHandle {
|
|
201
226
|
host: string;
|
package/lib/utils/logger.d.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import type { LoggerFunction } from '../types/index.js';
|
|
1
|
+
import type { LogLevel, LoggerFunction, LogThreshold } from '../types/index.js';
|
|
2
2
|
export declare function timestamp(): string;
|
|
3
|
+
/** True when a message at `level` passes `threshold`. */
|
|
4
|
+
export declare function passes(threshold: LogThreshold, level: LogLevel): boolean;
|
|
5
|
+
/** The console threshold when none is given: $JOVIAN_LOG_LEVEL if it names one, else 'notice'. */
|
|
6
|
+
export declare function defaultLogLevel(env?: Record<string, string | undefined>): LogThreshold;
|
|
3
7
|
export declare class Logger {
|
|
4
|
-
private customLogger
|
|
5
|
-
|
|
8
|
+
private readonly customLogger;
|
|
9
|
+
private readonly threshold;
|
|
10
|
+
/**
|
|
11
|
+
* A custom logger receives every message (it does its own filtering, as it
|
|
12
|
+
* always has); `threshold` only limits what is printed to the console.
|
|
13
|
+
*/
|
|
14
|
+
constructor(customLogger?: LoggerFunction, threshold?: LogThreshold);
|
|
15
|
+
private emit;
|
|
6
16
|
trace(message: string, data?: any): void;
|
|
7
17
|
debug(message: string, data?: any): void;
|
|
8
18
|
info(message: string, data?: any): void;
|
|
19
|
+
notice(message: string, data?: any): void;
|
|
9
20
|
warn(message: string, data?: any): void;
|
|
10
21
|
error(message: string, error?: any): void;
|
|
11
22
|
}
|
package/lib/utils/logger.js
CHANGED
|
@@ -9,50 +9,58 @@ export function timestamp() {
|
|
|
9
9
|
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
|
|
10
10
|
`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}`;
|
|
11
11
|
}
|
|
12
|
+
const ORDER = { trace: 0, debug: 1, info: 2, notice: 3, warn: 4, error: 5, silent: 6 };
|
|
13
|
+
/** True when a message at `level` passes `threshold`. */
|
|
14
|
+
export function passes(threshold, level) {
|
|
15
|
+
return ORDER[level] >= ORDER[threshold];
|
|
16
|
+
}
|
|
17
|
+
/** The console threshold when none is given: $JOVIAN_LOG_LEVEL if it names one, else 'notice'. */
|
|
18
|
+
export function defaultLogLevel(env = process.env) {
|
|
19
|
+
const wanted = env.JOVIAN_LOG_LEVEL?.trim().toLowerCase();
|
|
20
|
+
return wanted && wanted in ORDER ? wanted : 'notice';
|
|
21
|
+
}
|
|
12
22
|
export class Logger {
|
|
13
23
|
customLogger;
|
|
14
|
-
|
|
24
|
+
threshold;
|
|
25
|
+
/**
|
|
26
|
+
* A custom logger receives every message (it does its own filtering, as it
|
|
27
|
+
* always has); `threshold` only limits what is printed to the console.
|
|
28
|
+
*/
|
|
29
|
+
constructor(customLogger, threshold = defaultLogLevel()) {
|
|
15
30
|
this.customLogger = customLogger;
|
|
31
|
+
this.threshold = threshold;
|
|
16
32
|
}
|
|
17
|
-
|
|
33
|
+
emit(level, message, data) {
|
|
18
34
|
if (this.customLogger) {
|
|
19
|
-
this.customLogger(
|
|
20
|
-
|
|
21
|
-
else {
|
|
22
|
-
console.log(`${timestamp()} [trace] ${message}`, data ? data : '');
|
|
35
|
+
this.customLogger(level, message, data);
|
|
36
|
+
return;
|
|
23
37
|
}
|
|
38
|
+
if (!passes(this.threshold, level))
|
|
39
|
+
return;
|
|
40
|
+
const print = level === 'warn' ? console.warn : level === 'error' ? console.error : console.log;
|
|
41
|
+
const line = `${timestamp()} [${level}] ${message}`;
|
|
42
|
+
if (data)
|
|
43
|
+
print(line, data);
|
|
44
|
+
else
|
|
45
|
+
print(line);
|
|
46
|
+
}
|
|
47
|
+
trace(message, data) {
|
|
48
|
+
this.emit('trace', message, data);
|
|
24
49
|
}
|
|
25
50
|
debug(message, data) {
|
|
26
|
-
|
|
27
|
-
this.customLogger('debug', message, data);
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
console.log(`${timestamp()} [debug] ${message}`, data ? data : '');
|
|
31
|
-
}
|
|
51
|
+
this.emit('debug', message, data);
|
|
32
52
|
}
|
|
33
53
|
info(message, data) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log(`${timestamp()} [info] ${message}`, data ? data : '');
|
|
39
|
-
}
|
|
54
|
+
this.emit('info', message, data);
|
|
55
|
+
}
|
|
56
|
+
notice(message, data) {
|
|
57
|
+
this.emit('notice', message, data);
|
|
40
58
|
}
|
|
41
59
|
warn(message, data) {
|
|
42
|
-
|
|
43
|
-
this.customLogger('warn', message, data);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
console.warn(`${timestamp()} [warn] ${message}`, data ? data : '');
|
|
47
|
-
}
|
|
60
|
+
this.emit('warn', message, data);
|
|
48
61
|
}
|
|
49
62
|
error(message, error) {
|
|
50
|
-
|
|
51
|
-
this.customLogger('error', message, error);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
console.error(`${timestamp()} [error] ${message}`, error ? error : '');
|
|
55
|
-
}
|
|
63
|
+
this.emit('error', message, error);
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
//# sourceMappingURL=logger.js.map
|