@oh-my-pi/pi-coding-agent 14.9.5 → 14.9.8
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/CHANGELOG.md +69 -0
- package/package.json +7 -7
- package/scripts/generate-template.ts +4 -3
- package/src/cli/setup-cli.ts +14 -161
- package/src/cli/stats-cli.ts +56 -2
- package/src/cli.ts +0 -1
- package/src/config/settings-schema.ts +0 -10
- package/src/eval/eval.lark +30 -10
- package/src/eval/js/context-manager.ts +334 -564
- package/src/eval/js/shared/helpers.ts +237 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/shared/rewrite-imports.ts +211 -0
- package/src/eval/js/shared/runtime.ts +168 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +2 -4
- package/src/eval/js/worker-core.ts +146 -0
- package/src/eval/js/worker-entry.ts +24 -0
- package/src/eval/js/worker-protocol.ts +41 -0
- package/src/eval/parse.ts +218 -49
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +74 -89
- package/src/eval/py/index.ts +1 -2
- package/src/eval/py/kernel.ts +472 -900
- package/src/eval/py/prelude.py +95 -7
- package/src/eval/py/runner.py +879 -0
- package/src/eval/py/runtime.ts +3 -16
- package/src/eval/py/tool-bridge.ts +137 -0
- package/src/export/html/index.ts +5 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +93 -5
- package/src/export/html/template.macro.ts +4 -3
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/modes/components/read-tool-group.ts +9 -0
- package/src/modes/controllers/command-controller.ts +0 -23
- package/src/prompts/tools/eval.md +14 -27
- package/src/prompts/tools/read.md +1 -0
- package/src/session/agent-session.ts +0 -1
- package/src/session/history-storage.ts +77 -19
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +86 -5
- package/src/tools/browser/tab-worker.ts +104 -58
- package/src/tools/conflict-detect.ts +661 -0
- package/src/tools/eval.ts +1 -1
- package/src/tools/index.ts +6 -0
- package/src/tools/path-utils.ts +1 -1
- package/src/tools/read.ts +130 -0
- package/src/tools/write.ts +204 -0
- package/src/web/search/index.ts +6 -4
- package/src/cli/jupyter-cli.ts +0 -106
- package/src/commands/jupyter.ts +0 -32
- package/src/eval/py/cancellation.ts +0 -28
- package/src/eval/py/gateway-coordinator.ts +0 -424
- /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
- /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
package/src/eval/py/kernel.ts
CHANGED
|
@@ -1,163 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Subprocess-backed Python runner.
|
|
3
|
+
*
|
|
4
|
+
* Speaks NDJSON with `runner.py` over stdin/stdout. One subprocess per kernel
|
|
5
|
+
* instance; sessions reuse a single subprocess across executions. Cancellation
|
|
6
|
+
* is `kill("SIGINT")` which raises a real `KeyboardInterrupt` inside user
|
|
7
|
+
* code. Shutdown writes `{"type":"exit"}` and escalates to SIGTERM/SIGKILL on
|
|
8
|
+
* timeout.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { $flag, isBunTestRuntime, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import type { Subprocess } from "bun";
|
|
2
15
|
import { $ } from "bun";
|
|
3
16
|
import { Settings } from "../../config/settings";
|
|
4
|
-
import {
|
|
5
|
-
import { createCancellationError, getAbortReason, getExecutionCancellationError } from "./cancellation";
|
|
6
|
-
import { acquireSharedGateway, releaseSharedGateway, shutdownSharedGateway } from "./gateway-coordinator";
|
|
7
|
-
|
|
17
|
+
import { type KernelDisplayOutput, renderKernelDisplay } from "./display";
|
|
8
18
|
import { PYTHON_PRELUDE } from "./prelude";
|
|
19
|
+
import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
|
|
9
20
|
import { filterEnv, resolvePythonRuntime } from "./runtime";
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const TRACE_IPC = $flag("PI_PYTHON_IPC_TRACE");
|
|
14
|
-
|
|
15
|
-
class SharedGatewayCreateError extends Error {
|
|
16
|
-
constructor(
|
|
17
|
-
readonly status: number,
|
|
18
|
-
message: string,
|
|
19
|
-
) {
|
|
20
|
-
super(message);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface ExternalGatewayConfig {
|
|
25
|
-
url: string;
|
|
26
|
-
token?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getExternalGatewayConfig(): ExternalGatewayConfig | null {
|
|
30
|
-
const url = $env.PI_PYTHON_GATEWAY_URL;
|
|
31
|
-
if (!url) return null;
|
|
32
|
-
return {
|
|
33
|
-
url: url.replace(/\/$/, ""),
|
|
34
|
-
token: $env.PI_PYTHON_GATEWAY_TOKEN,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const STARTUP_CLEANUP_TIMEOUT_MS = 2_000;
|
|
39
|
-
const WEBSOCKET_CONNECT_TIMEOUT_MS = 10_000;
|
|
40
|
-
|
|
41
|
-
interface KernelLifecycleOptions {
|
|
42
|
-
signal?: AbortSignal;
|
|
43
|
-
deadlineMs?: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface KernelShutdownOptions {
|
|
47
|
-
signal?: AbortSignal;
|
|
48
|
-
timeoutMs?: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface KernelShutdownResult {
|
|
52
|
-
confirmed: boolean;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getRemainingTimeMs(deadlineMs?: number): number | undefined {
|
|
56
|
-
if (deadlineMs === undefined) return undefined;
|
|
57
|
-
return Math.max(0, deadlineMs - Date.now());
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function throwIfStartupExecutionFailed(
|
|
61
|
-
result: Pick<KernelExecuteResult, "cancelled" | "status" | "timedOut">,
|
|
62
|
-
signal: AbortSignal | undefined,
|
|
63
|
-
failureMessage: string,
|
|
64
|
-
): void {
|
|
65
|
-
if (result.cancelled) {
|
|
66
|
-
throw getExecutionCancellationError(result, signal, failureMessage);
|
|
67
|
-
}
|
|
68
|
-
if (result.status === "error") {
|
|
69
|
-
throw new Error(failureMessage);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function createAbortedSignal(reason: Error): AbortSignal {
|
|
74
|
-
const controller = new AbortController();
|
|
75
|
-
controller.abort(reason);
|
|
76
|
-
return controller.signal;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function combineAbortSignal(
|
|
80
|
-
options: KernelLifecycleOptions,
|
|
81
|
-
timeoutCapMs?: number,
|
|
82
|
-
fallbackReason = "Operation aborted",
|
|
83
|
-
): AbortSignal | undefined {
|
|
84
|
-
if (options.signal?.aborted) {
|
|
85
|
-
return options.signal;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const signals: AbortSignal[] = [];
|
|
89
|
-
if (options.signal) {
|
|
90
|
-
signals.push(options.signal);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const remainingMs = getRemainingTimeMs(options.deadlineMs);
|
|
94
|
-
const timeoutMs =
|
|
95
|
-
remainingMs === undefined
|
|
96
|
-
? timeoutCapMs
|
|
97
|
-
: timeoutCapMs === undefined
|
|
98
|
-
? remainingMs
|
|
99
|
-
: Math.min(remainingMs, timeoutCapMs);
|
|
100
|
-
|
|
101
|
-
if (timeoutMs !== undefined) {
|
|
102
|
-
if (timeoutMs <= 0) {
|
|
103
|
-
return createAbortedSignal(createCancellationError("TimeoutError", fallbackReason));
|
|
104
|
-
}
|
|
105
|
-
signals.push(AbortSignal.timeout(timeoutMs));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (signals.length === 0) return undefined;
|
|
109
|
-
return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function throwIfAborted(signal: AbortSignal | undefined, fallbackReason: string): void {
|
|
113
|
-
if (!signal?.aborted) return;
|
|
114
|
-
throw getAbortReason(signal, fallbackReason);
|
|
115
|
-
}
|
|
22
|
+
export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
|
|
23
|
+
export { renderKernelDisplay } from "./display";
|
|
116
24
|
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
signal: combineAbortSignal(options, undefined, "Python kernel startup aborted"),
|
|
120
|
-
timeoutMs: getRemainingTimeMs(options.deadlineMs),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function getStartupCleanupTimeoutMs(deadlineMs?: number): number {
|
|
125
|
-
const remainingMs = getRemainingTimeMs(deadlineMs);
|
|
126
|
-
if (remainingMs === undefined || remainingMs <= 0) return STARTUP_CLEANUP_TIMEOUT_MS;
|
|
127
|
-
return Math.min(STARTUP_CLEANUP_TIMEOUT_MS, remainingMs);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface JupyterHeader {
|
|
131
|
-
msg_id: string;
|
|
132
|
-
session: string;
|
|
133
|
-
username: string;
|
|
134
|
-
date: string;
|
|
135
|
-
msg_type: string;
|
|
136
|
-
version: string;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export interface JupyterMessage {
|
|
140
|
-
channel: string;
|
|
141
|
-
header: JupyterHeader;
|
|
142
|
-
parent_header: Record<string, unknown>;
|
|
143
|
-
metadata: Record<string, unknown>;
|
|
144
|
-
content: Record<string, unknown>;
|
|
145
|
-
buffers?: Uint8Array[];
|
|
146
|
-
}
|
|
25
|
+
const TRACE_IPC = $flag("PI_PYTHON_IPC_TRACE");
|
|
147
26
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
27
|
+
// Cache the runner script on disk so the subprocess loads it normally. Cached
|
|
28
|
+
// per script hash so installs don't race across versions.
|
|
29
|
+
const RUNNER_CACHE_DIR = path.join(os.tmpdir(), "omp-python-runner");
|
|
30
|
+
let RUNNER_SCRIPT_PATH: string | null = null;
|
|
31
|
+
|
|
32
|
+
async function ensureRunnerScript(): Promise<string> {
|
|
33
|
+
if (RUNNER_SCRIPT_PATH) return RUNNER_SCRIPT_PATH;
|
|
34
|
+
await fs.promises.mkdir(RUNNER_CACHE_DIR, { recursive: true });
|
|
35
|
+
const hash = Bun.hash(RUNNER_SCRIPT).toString(36);
|
|
36
|
+
const target = path.join(RUNNER_CACHE_DIR, `runner-${hash}.py`);
|
|
37
|
+
if (!fs.existsSync(target)) {
|
|
38
|
+
await Bun.write(target, RUNNER_SCRIPT);
|
|
39
|
+
}
|
|
40
|
+
RUNNER_SCRIPT_PATH = target;
|
|
41
|
+
return target;
|
|
154
42
|
}
|
|
155
43
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
| { type: "image"; data: string; mimeType: string }
|
|
159
|
-
| { type: "markdown" }
|
|
160
|
-
| { type: "status"; event: PythonStatusEvent };
|
|
44
|
+
const SHUTDOWN_GRACE_MS = 1_000;
|
|
45
|
+
const STARTUP_TIMEOUT_MS = 10_000;
|
|
161
46
|
|
|
162
47
|
export interface KernelExecuteOptions {
|
|
163
48
|
signal?: AbortSignal;
|
|
@@ -178,10 +63,23 @@ export interface KernelExecuteResult {
|
|
|
178
63
|
stdinRequested: boolean;
|
|
179
64
|
}
|
|
180
65
|
|
|
66
|
+
export interface KernelShutdownResult {
|
|
67
|
+
confirmed: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface KernelLifecycleOptions {
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
deadlineMs?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
181
75
|
interface KernelStartOptions extends KernelLifecycleOptions {
|
|
182
76
|
cwd: string;
|
|
183
77
|
env?: Record<string, string | undefined>;
|
|
184
|
-
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface KernelShutdownOptions {
|
|
81
|
+
signal?: AbortSignal;
|
|
82
|
+
timeoutMs?: number;
|
|
185
83
|
}
|
|
186
84
|
|
|
187
85
|
export interface PythonKernelAvailability {
|
|
@@ -190,231 +88,91 @@ export interface PythonKernelAvailability {
|
|
|
190
88
|
reason?: string;
|
|
191
89
|
}
|
|
192
90
|
|
|
91
|
+
function getRemainingTimeMs(deadlineMs?: number): number | undefined {
|
|
92
|
+
if (deadlineMs === undefined) return undefined;
|
|
93
|
+
return Math.max(0, deadlineMs - Date.now());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createAbortError(name: "AbortError" | "TimeoutError", message: string): Error {
|
|
97
|
+
const err = new Error(message);
|
|
98
|
+
err.name = name;
|
|
99
|
+
return err;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function throwIfAborted(signal: AbortSignal | undefined, fallbackReason: string): void {
|
|
103
|
+
if (!signal?.aborted) return;
|
|
104
|
+
const reason = signal.reason;
|
|
105
|
+
if (reason instanceof Error) throw reason;
|
|
106
|
+
throw createAbortError("AbortError", typeof reason === "string" ? reason : fallbackReason);
|
|
107
|
+
}
|
|
108
|
+
|
|
193
109
|
export async function checkPythonKernelAvailability(cwd: string): Promise<PythonKernelAvailability> {
|
|
194
110
|
if (isBunTestRuntime() || $flag("PI_PYTHON_SKIP_CHECK")) {
|
|
195
111
|
return { ok: true };
|
|
196
112
|
}
|
|
197
|
-
|
|
198
|
-
const externalConfig = getExternalGatewayConfig();
|
|
199
|
-
if (externalConfig) {
|
|
200
|
-
return checkExternalGatewayAvailability(externalConfig);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
113
|
try {
|
|
204
114
|
const settings = await Settings.init();
|
|
205
115
|
const { env } = settings.getShellConfig();
|
|
206
116
|
const baseEnv = filterEnv(env);
|
|
207
117
|
const runtime = resolvePythonRuntime(cwd, baseEnv);
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
118
|
+
const probe = await $`${runtime.pythonPath} -c "import sys;sys.exit(0)"`
|
|
119
|
+
.quiet()
|
|
120
|
+
.nothrow()
|
|
121
|
+
.cwd(cwd)
|
|
122
|
+
.env(runtime.env);
|
|
123
|
+
if (probe.exitCode === 0) {
|
|
212
124
|
return { ok: true, pythonPath: runtime.pythonPath };
|
|
213
125
|
}
|
|
214
126
|
return {
|
|
215
127
|
ok: false,
|
|
216
128
|
pythonPath: runtime.pythonPath,
|
|
217
|
-
reason:
|
|
218
|
-
"kernel_gateway (jupyter-kernel-gateway) or ipykernel not installed. Run: python -m pip install jupyter_kernel_gateway ipykernel",
|
|
129
|
+
reason: `Python interpreter at ${runtime.pythonPath} returned exit code ${probe.exitCode}`,
|
|
219
130
|
};
|
|
220
|
-
} catch (err
|
|
131
|
+
} catch (err) {
|
|
221
132
|
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
222
133
|
}
|
|
223
134
|
}
|
|
224
135
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (response.ok) {
|
|
240
|
-
return { ok: true };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (response.status === 401 || response.status === 403) {
|
|
244
|
-
return {
|
|
245
|
-
ok: false,
|
|
246
|
-
reason: `External gateway at ${config.url} requires authentication. Set PI_PYTHON_GATEWAY_TOKEN.`,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
ok: false,
|
|
252
|
-
reason: `External gateway at ${config.url} returned status ${response.status}`,
|
|
253
|
-
};
|
|
254
|
-
} catch (err: unknown) {
|
|
255
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
256
|
-
if (message.includes("abort") || message.includes("timeout")) {
|
|
257
|
-
return {
|
|
258
|
-
ok: false,
|
|
259
|
-
reason: `External gateway at ${config.url} is not reachable (timeout)`,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
return {
|
|
263
|
-
ok: false,
|
|
264
|
-
reason: `External gateway at ${config.url} is not reachable: ${message}`,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function normalizeDisplayText(text: string): string {
|
|
270
|
-
return text.endsWith("\n") ? text : `${text}\n`;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** Renders a Jupyter display_data message into text and structured outputs. */
|
|
274
|
-
export async function renderKernelDisplay(content: Record<string, unknown>): Promise<{
|
|
275
|
-
text: string;
|
|
276
|
-
outputs: KernelDisplayOutput[];
|
|
277
|
-
}> {
|
|
278
|
-
const data = content.data as Record<string, unknown> | undefined;
|
|
279
|
-
if (!data) return { text: "", outputs: [] };
|
|
280
|
-
|
|
281
|
-
const outputs: KernelDisplayOutput[] = [];
|
|
282
|
-
|
|
283
|
-
// Handle status events (custom MIME type from prelude helpers)
|
|
284
|
-
if (data["application/x-omp-status"] !== undefined) {
|
|
285
|
-
const statusData = data["application/x-omp-status"];
|
|
286
|
-
if (statusData && typeof statusData === "object" && "op" in statusData) {
|
|
287
|
-
outputs.push({ type: "status", event: statusData as PythonStatusEvent });
|
|
288
|
-
}
|
|
289
|
-
// Status events don't produce text output
|
|
290
|
-
return { text: "", outputs };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (typeof data["image/png"] === "string") {
|
|
294
|
-
outputs.push({ type: "image", data: data["image/png"] as string, mimeType: "image/png" });
|
|
295
|
-
}
|
|
296
|
-
if (data["application/json"] !== undefined) {
|
|
297
|
-
outputs.push({ type: "json", data: data["application/json"] });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Check text/markdown before text/plain since Markdown objects provide both
|
|
301
|
-
// (text/plain is just the repr)
|
|
302
|
-
if (typeof data["text/markdown"] === "string") {
|
|
303
|
-
outputs.push({ type: "markdown" });
|
|
304
|
-
return { text: normalizeDisplayText(String(data["text/markdown"])), outputs };
|
|
305
|
-
}
|
|
306
|
-
if (typeof data["text/plain"] === "string") {
|
|
307
|
-
return { text: normalizeDisplayText(String(data["text/plain"])), outputs };
|
|
308
|
-
}
|
|
309
|
-
if (data["text/html"] !== undefined) {
|
|
310
|
-
const markdown = (await htmlToBasicMarkdown(String(data["text/html"]))) || "";
|
|
311
|
-
return { text: markdown ? normalizeDisplayText(markdown) : "", outputs };
|
|
312
|
-
}
|
|
313
|
-
return { text: "", outputs };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function deserializeWebSocketMessage(data: ArrayBuffer): JupyterMessage | null {
|
|
317
|
-
const view = new DataView(data);
|
|
318
|
-
const offsetCount = view.getUint32(0, true);
|
|
319
|
-
|
|
320
|
-
if (offsetCount < 1) return null;
|
|
321
|
-
|
|
322
|
-
const offsets: number[] = [];
|
|
323
|
-
for (let i = 0; i < offsetCount; i++) {
|
|
324
|
-
offsets.push(view.getUint32(4 + i * 4, true));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const msgStart = offsets[0];
|
|
328
|
-
const msgEnd = offsets.length > 1 ? offsets[1] : data.byteLength;
|
|
329
|
-
const msgBytes = new Uint8Array(data, msgStart, msgEnd - msgStart);
|
|
330
|
-
const msgText = TEXT_DECODER.decode(msgBytes);
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
const msg = JSON.parse(msgText) as {
|
|
334
|
-
channel: string;
|
|
335
|
-
header: JupyterHeader;
|
|
336
|
-
parent_header: Record<string, unknown>;
|
|
337
|
-
metadata: Record<string, unknown>;
|
|
338
|
-
content: Record<string, unknown>;
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const buffers: Uint8Array[] = [];
|
|
342
|
-
for (let i = 1; i < offsets.length; i++) {
|
|
343
|
-
const start = offsets[i];
|
|
344
|
-
const end = i + 1 < offsets.length ? offsets[i + 1] : data.byteLength;
|
|
345
|
-
buffers.push(new Uint8Array(data, start, end - start));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return { ...msg, buffers };
|
|
349
|
-
} catch {
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
136
|
+
type FrameType = "started" | "stdout" | "stderr" | "display" | "result" | "error" | "done";
|
|
137
|
+
|
|
138
|
+
interface Frame {
|
|
139
|
+
type: FrameType;
|
|
140
|
+
id?: string;
|
|
141
|
+
data?: string;
|
|
142
|
+
bundle?: Record<string, unknown>;
|
|
143
|
+
ename?: string;
|
|
144
|
+
evalue?: string;
|
|
145
|
+
traceback?: string[];
|
|
146
|
+
status?: "ok" | "error";
|
|
147
|
+
executionCount?: number;
|
|
148
|
+
cancelled?: boolean;
|
|
352
149
|
}
|
|
353
150
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const offsetCount = 1 + buffers.length;
|
|
365
|
-
const headerSize = 4 + offsetCount * 4;
|
|
366
|
-
const msgBytes = Buffer.byteLength(msgText);
|
|
367
|
-
let totalSize = headerSize + msgBytes;
|
|
368
|
-
for (const buf of buffers) {
|
|
369
|
-
totalSize += buf.length;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const result = new ArrayBuffer(totalSize);
|
|
373
|
-
const view = new DataView(result);
|
|
374
|
-
const bytes = new Uint8Array(result);
|
|
375
|
-
|
|
376
|
-
view.setUint32(0, offsetCount, true);
|
|
377
|
-
|
|
378
|
-
let offset = headerSize;
|
|
379
|
-
view.setUint32(4, offset, true);
|
|
380
|
-
TEXT_ENCODER.encodeInto(msgText, bytes.subarray(offset));
|
|
381
|
-
offset += msgBytes;
|
|
382
|
-
|
|
383
|
-
for (let i = 0; i < buffers.length; i++) {
|
|
384
|
-
view.setUint32(4 + (i + 1) * 4, offset, true);
|
|
385
|
-
bytes.set(buffers[i], offset);
|
|
386
|
-
offset += buffers[i].length;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return result;
|
|
151
|
+
interface PendingExecution {
|
|
152
|
+
resolve: (result: KernelExecuteResult) => void;
|
|
153
|
+
options?: KernelExecuteOptions;
|
|
154
|
+
status: "ok" | "error";
|
|
155
|
+
executionCount?: number;
|
|
156
|
+
error?: { name: string; value: string; traceback: string[] };
|
|
157
|
+
cancelled: boolean;
|
|
158
|
+
timedOut: boolean;
|
|
159
|
+
stdinRequested: boolean;
|
|
160
|
+
settled: boolean;
|
|
390
161
|
}
|
|
391
162
|
|
|
392
163
|
export class PythonKernel {
|
|
393
|
-
readonly
|
|
394
|
-
|
|
395
|
-
#
|
|
396
|
-
#disposed = false;
|
|
164
|
+
readonly id: string;
|
|
165
|
+
#proc: Subprocess | null = null;
|
|
166
|
+
#stdin: Bun.FileSink | null = null;
|
|
397
167
|
#alive = true;
|
|
398
|
-
#
|
|
168
|
+
#disposed = false;
|
|
399
169
|
#shutdownConfirmed = false;
|
|
400
|
-
#
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
private constructor(
|
|
404
|
-
readonly id: string,
|
|
405
|
-
readonly kernelId: string,
|
|
406
|
-
readonly gatewayUrl: string,
|
|
407
|
-
readonly sessionId: string,
|
|
408
|
-
readonly username: string,
|
|
409
|
-
readonly isSharedGateway: boolean,
|
|
410
|
-
authToken?: string,
|
|
411
|
-
) {
|
|
412
|
-
this.#authToken = authToken;
|
|
413
|
-
}
|
|
170
|
+
#exitedPromise: Promise<number> | null = null;
|
|
171
|
+
#pending = new Map<string, PendingExecution>();
|
|
172
|
+
#readBuffer = "";
|
|
414
173
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return { Authorization: `token ${this.#authToken}` };
|
|
174
|
+
private constructor(id: string) {
|
|
175
|
+
this.id = id;
|
|
418
176
|
}
|
|
419
177
|
|
|
420
178
|
static async start(options: KernelStartOptions): Promise<PythonKernel> {
|
|
@@ -427,629 +185,443 @@ export class PythonKernel {
|
|
|
427
185
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
428
186
|
}
|
|
429
187
|
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
} catch (err) {
|
|
463
|
-
logger.debug("PythonKernel.start:sharedFailed");
|
|
464
|
-
if (attempt === 0 && err instanceof SharedGatewayCreateError && err.status >= 500) {
|
|
465
|
-
logger.warn("Shared gateway kernel creation failed, retrying", {
|
|
466
|
-
status: err.status,
|
|
467
|
-
});
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
logger.warn("Failed to acquire shared gateway", {
|
|
471
|
-
error: err instanceof Error ? err.message : String(err),
|
|
472
|
-
});
|
|
473
|
-
throw err;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
throw new Error("Shared Python gateway unavailable after retry");
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
static async #startWithExternalGateway(
|
|
481
|
-
config: ExternalGatewayConfig,
|
|
482
|
-
cwd: string,
|
|
483
|
-
env?: Record<string, string | undefined>,
|
|
484
|
-
startup: KernelLifecycleOptions = {},
|
|
485
|
-
): Promise<PythonKernel> {
|
|
486
|
-
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
487
|
-
if (config.token) {
|
|
488
|
-
headers.Authorization = `token ${config.token}`;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const startupSignal = combineAbortSignal(startup, undefined, "Python kernel startup aborted");
|
|
492
|
-
throwIfAborted(startupSignal, "Python kernel startup aborted");
|
|
493
|
-
const createResponse = await fetch(`${config.url}/api/kernels`, {
|
|
494
|
-
method: "POST",
|
|
495
|
-
headers,
|
|
496
|
-
body: JSON.stringify({ name: "python3" }),
|
|
497
|
-
signal: startupSignal,
|
|
188
|
+
const settings = await Settings.init();
|
|
189
|
+
const { env: shellEnv } = settings.getShellConfig();
|
|
190
|
+
const baseEnv = filterEnv(shellEnv);
|
|
191
|
+
const runtime = resolvePythonRuntime(options.cwd, baseEnv);
|
|
192
|
+
const spawnEnv: Record<string, string> = {};
|
|
193
|
+
for (const [key, value] of Object.entries(runtime.env)) {
|
|
194
|
+
if (typeof value === "string") spawnEnv[key] = value;
|
|
195
|
+
}
|
|
196
|
+
for (const [key, value] of Object.entries(options.env ?? {})) {
|
|
197
|
+
if (typeof value === "string") spawnEnv[key] = value;
|
|
198
|
+
}
|
|
199
|
+
// Unbuffered IO is critical for streaming.
|
|
200
|
+
spawnEnv.PYTHONUNBUFFERED = "1";
|
|
201
|
+
spawnEnv.PYTHONIOENCODING = "utf-8";
|
|
202
|
+
|
|
203
|
+
const scriptPath = await ensureRunnerScript();
|
|
204
|
+
const kernel = new PythonKernel(Snowflake.next());
|
|
205
|
+
|
|
206
|
+
const proc = Bun.spawn([runtime.pythonPath, "-u", scriptPath], {
|
|
207
|
+
cwd: options.cwd,
|
|
208
|
+
env: spawnEnv,
|
|
209
|
+
stdin: "pipe",
|
|
210
|
+
stdout: "pipe",
|
|
211
|
+
stderr: "pipe",
|
|
212
|
+
windowsHide: true,
|
|
213
|
+
});
|
|
214
|
+
kernel.#proc = proc;
|
|
215
|
+
kernel.#stdin = proc.stdin;
|
|
216
|
+
kernel.#exitedPromise = proc.exited;
|
|
217
|
+
void kernel.#exitedPromise.then(code => {
|
|
218
|
+
kernel.#alive = false;
|
|
219
|
+
kernel.#abortPendingExecutions(`Python kernel exited with code ${code}`);
|
|
498
220
|
});
|
|
499
221
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const kernelInfo = (await createResponse.json()) as { id: string };
|
|
505
|
-
const kernelId = kernelInfo.id;
|
|
222
|
+
kernel.#startReader(proc.stdout as ReadableStream<Uint8Array>);
|
|
223
|
+
kernel.#startStderrDrain(proc.stderr as ReadableStream<Uint8Array>);
|
|
506
224
|
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
kernelId,
|
|
510
|
-
config.url,
|
|
511
|
-
Snowflake.next(),
|
|
512
|
-
"omp",
|
|
513
|
-
false,
|
|
514
|
-
config.token,
|
|
515
|
-
);
|
|
225
|
+
const startup = { signal: options.signal, deadlineMs: options.deadlineMs };
|
|
226
|
+
const startupBudget = Math.min(getRemainingTimeMs(startup.deadlineMs) ?? STARTUP_TIMEOUT_MS, STARTUP_TIMEOUT_MS);
|
|
516
227
|
|
|
517
228
|
try {
|
|
518
|
-
|
|
519
|
-
await kernel.#
|
|
520
|
-
|
|
521
|
-
const preludeResult = await kernel.execute(PYTHON_PRELUDE, {
|
|
522
|
-
...preludeOptions,
|
|
523
|
-
silent: true,
|
|
524
|
-
storeHistory: false,
|
|
525
|
-
});
|
|
526
|
-
throwIfStartupExecutionFailed(
|
|
527
|
-
preludeResult,
|
|
528
|
-
preludeOptions.signal,
|
|
529
|
-
"Failed to initialize Python kernel prelude",
|
|
530
|
-
);
|
|
531
|
-
|
|
229
|
+
const initScript = buildInitScript(options.cwd, options.env);
|
|
230
|
+
await kernel.#executeWithBudget(initScript, startup.signal, startupBudget, "Python kernel init");
|
|
231
|
+
await kernel.#executeWithBudget(PYTHON_PRELUDE, startup.signal, startupBudget, "Python kernel prelude");
|
|
532
232
|
return kernel;
|
|
533
|
-
} catch (err
|
|
534
|
-
await kernel.shutdown({ timeoutMs:
|
|
233
|
+
} catch (err) {
|
|
234
|
+
await kernel.shutdown({ timeoutMs: SHUTDOWN_GRACE_MS }).catch(() => {});
|
|
535
235
|
throw err;
|
|
536
236
|
}
|
|
537
237
|
}
|
|
538
238
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
cwd: string,
|
|
542
|
-
env?: Record<string, string | undefined>,
|
|
543
|
-
startup: KernelLifecycleOptions = {},
|
|
544
|
-
): Promise<PythonKernel> {
|
|
545
|
-
const startupSignal = combineAbortSignal(startup, undefined, "Python kernel startup aborted");
|
|
546
|
-
throwIfAborted(startupSignal, "Python kernel startup aborted");
|
|
547
|
-
const createResponse = await logger.time(
|
|
548
|
-
"startWithSharedGateway:createKernel",
|
|
549
|
-
fetch,
|
|
550
|
-
`${gatewayUrl}/api/kernels`,
|
|
551
|
-
{
|
|
552
|
-
method: "POST",
|
|
553
|
-
headers: { "Content-Type": "application/json" },
|
|
554
|
-
body: JSON.stringify({ name: "python3" }),
|
|
555
|
-
signal: startupSignal,
|
|
556
|
-
},
|
|
557
|
-
);
|
|
558
|
-
|
|
559
|
-
if (!createResponse.ok) {
|
|
560
|
-
logger.debug(`sharedGateway:fetch:notOk:${createResponse.status}`);
|
|
561
|
-
await shutdownSharedGateway();
|
|
562
|
-
const text = await createResponse.text();
|
|
563
|
-
throw new SharedGatewayCreateError(
|
|
564
|
-
createResponse.status,
|
|
565
|
-
`Failed to create kernel on shared gateway: ${text}`,
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const kernelInfo = (await logger.time(
|
|
570
|
-
"startWithSharedGateway:parseJson",
|
|
571
|
-
createResponse.json.bind(createResponse),
|
|
572
|
-
)) as { id: string };
|
|
573
|
-
const kernelId = kernelInfo.id;
|
|
574
|
-
|
|
575
|
-
const kernel = new PythonKernel(Snowflake.next(), kernelId, gatewayUrl, Snowflake.next(), "omp", true);
|
|
576
|
-
|
|
577
|
-
try {
|
|
578
|
-
await logger.time("startWithSharedGateway:connectWS", kernel.#connectWebSocket.bind(kernel), startup);
|
|
579
|
-
await logger.time(
|
|
580
|
-
"startWithSharedGateway:initEnv",
|
|
581
|
-
kernel.#initializeKernelEnvironment.bind(kernel),
|
|
582
|
-
cwd,
|
|
583
|
-
env,
|
|
584
|
-
startup,
|
|
585
|
-
);
|
|
586
|
-
const preludeOptions = getStartupExecuteOptions(startup);
|
|
587
|
-
const preludeResult = await logger.time(
|
|
588
|
-
"startWithSharedGateway:prelude",
|
|
589
|
-
kernel.execute.bind(kernel),
|
|
590
|
-
PYTHON_PRELUDE,
|
|
591
|
-
{
|
|
592
|
-
...preludeOptions,
|
|
593
|
-
silent: true,
|
|
594
|
-
storeHistory: false,
|
|
595
|
-
},
|
|
596
|
-
);
|
|
597
|
-
throwIfStartupExecutionFailed(
|
|
598
|
-
preludeResult,
|
|
599
|
-
preludeOptions.signal,
|
|
600
|
-
"Failed to initialize Python kernel prelude",
|
|
601
|
-
);
|
|
602
|
-
|
|
603
|
-
return kernel;
|
|
604
|
-
} catch (err: unknown) {
|
|
605
|
-
await kernel.shutdown({ timeoutMs: getStartupCleanupTimeoutMs(startup.deadlineMs) });
|
|
606
|
-
throw err;
|
|
607
|
-
}
|
|
239
|
+
isAlive(): boolean {
|
|
240
|
+
return this.#alive && !this.#disposed;
|
|
608
241
|
}
|
|
609
242
|
|
|
610
|
-
async
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (this.#authToken) {
|
|
614
|
-
wsUrl += `?token=${encodeURIComponent(this.#authToken)}`;
|
|
243
|
+
async execute(code: string, options?: KernelExecuteOptions): Promise<KernelExecuteResult> {
|
|
244
|
+
if (!this.isAlive()) {
|
|
245
|
+
throw new Error("Python kernel is not running");
|
|
615
246
|
}
|
|
616
247
|
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
connectSignal.removeEventListener("abort", onAbort);
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
|
|
631
|
-
const onAbort = () => {
|
|
632
|
-
ws.close();
|
|
633
|
-
if (settled) return;
|
|
634
|
-
settled = true;
|
|
635
|
-
finalize();
|
|
636
|
-
reject(getAbortReason(connectSignal, "WebSocket connection timeout"));
|
|
248
|
+
const msgId = Snowflake.next();
|
|
249
|
+
const { promise, resolve } = Promise.withResolvers<KernelExecuteResult>();
|
|
250
|
+
const pending: PendingExecution = {
|
|
251
|
+
resolve,
|
|
252
|
+
options,
|
|
253
|
+
status: "ok",
|
|
254
|
+
cancelled: false,
|
|
255
|
+
timedOut: false,
|
|
256
|
+
stdinRequested: false,
|
|
257
|
+
settled: false,
|
|
637
258
|
};
|
|
259
|
+
this.#pending.set(msgId, pending);
|
|
638
260
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
261
|
+
const finalize = () => {
|
|
262
|
+
if (pending.settled) return;
|
|
263
|
+
pending.settled = true;
|
|
264
|
+
this.#pending.delete(msgId);
|
|
265
|
+
cleanup();
|
|
266
|
+
resolve({
|
|
267
|
+
status: pending.status,
|
|
268
|
+
executionCount: pending.executionCount,
|
|
269
|
+
error: pending.error,
|
|
270
|
+
cancelled: pending.cancelled,
|
|
271
|
+
timedOut: pending.timedOut,
|
|
272
|
+
stdinRequested: pending.stdinRequested,
|
|
273
|
+
});
|
|
649
274
|
};
|
|
650
275
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
finalize();
|
|
656
|
-
reject(error);
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
this.#alive = false;
|
|
660
|
-
this.#ws = null;
|
|
661
|
-
this.#abortPendingExecutions(error.message);
|
|
276
|
+
const onAbort = () => {
|
|
277
|
+
pending.cancelled = true;
|
|
278
|
+
pending.timedOut = pending.timedOut || isTimeoutReason(options?.signal?.reason);
|
|
279
|
+
void this.interrupt();
|
|
662
280
|
};
|
|
281
|
+
const timeoutId =
|
|
282
|
+
typeof options?.timeoutMs === "number" && options.timeoutMs > 0
|
|
283
|
+
? setTimeout(() => {
|
|
284
|
+
pending.timedOut = true;
|
|
285
|
+
pending.cancelled = true;
|
|
286
|
+
void this.interrupt();
|
|
287
|
+
}, options.timeoutMs)
|
|
288
|
+
: undefined;
|
|
663
289
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (!settled) {
|
|
668
|
-
settled = true;
|
|
669
|
-
finalize();
|
|
670
|
-
reject(new Error("WebSocket closed before connection"));
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
this.#abortPendingExecutions("WebSocket closed");
|
|
290
|
+
const cleanup = () => {
|
|
291
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
292
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
674
293
|
};
|
|
675
294
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
try {
|
|
682
|
-
msg = JSON.parse(event.data) as JupyterMessage;
|
|
683
|
-
} catch {
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
295
|
+
if (options?.signal) {
|
|
296
|
+
if (options.signal.aborted) {
|
|
297
|
+
onAbort();
|
|
298
|
+
} else {
|
|
299
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
686
300
|
}
|
|
687
|
-
|
|
301
|
+
}
|
|
688
302
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
303
|
+
// Stash finalize on the pending entry so the reader can call it on `done`.
|
|
304
|
+
(pending as PendingExecution & { finalize: () => void }).finalize = finalize;
|
|
692
305
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
306
|
+
const payload = JSON.stringify({
|
|
307
|
+
id: msgId,
|
|
308
|
+
code,
|
|
309
|
+
silent: options?.silent ?? false,
|
|
310
|
+
storeHistory: options?.storeHistory ?? !(options?.silent ?? false),
|
|
311
|
+
});
|
|
698
312
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
313
|
+
try {
|
|
314
|
+
await this.#writeLine(payload);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
pending.cancelled = true;
|
|
317
|
+
pending.error = {
|
|
318
|
+
name: "TransportError",
|
|
319
|
+
value: err instanceof Error ? err.message : String(err),
|
|
320
|
+
traceback: [],
|
|
321
|
+
};
|
|
322
|
+
finalize();
|
|
323
|
+
}
|
|
706
324
|
|
|
707
325
|
return promise;
|
|
708
326
|
}
|
|
709
327
|
|
|
710
|
-
async
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const envPayload = Object.fromEntries(envEntries);
|
|
717
|
-
const initScript = [
|
|
718
|
-
"import os, sys",
|
|
719
|
-
`__omp_cwd = ${JSON.stringify(cwd)}`,
|
|
720
|
-
"os.chdir(__omp_cwd)",
|
|
721
|
-
`__omp_env = ${JSON.stringify(envPayload)}`,
|
|
722
|
-
"for __omp_key, __omp_val in __omp_env.items():\n os.environ[__omp_key] = __omp_val",
|
|
723
|
-
"if __omp_cwd not in sys.path:\n sys.path.insert(0, __omp_cwd)",
|
|
724
|
-
].join("\n");
|
|
725
|
-
const executeOptions = getStartupExecuteOptions(options);
|
|
726
|
-
const result = await this.execute(initScript, {
|
|
727
|
-
...executeOptions,
|
|
728
|
-
silent: true,
|
|
729
|
-
storeHistory: false,
|
|
730
|
-
});
|
|
731
|
-
throwIfStartupExecutionFailed(result, executeOptions.signal, "Failed to initialize Python kernel environment");
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
#abortPendingExecutions(reason: string): void {
|
|
735
|
-
if (this.#pendingExecutions.size === 0) return;
|
|
736
|
-
for (const cancel of this.#pendingExecutions.values()) {
|
|
737
|
-
cancel(reason);
|
|
328
|
+
async interrupt(): Promise<void> {
|
|
329
|
+
if (!this.#proc || this.#disposed) return;
|
|
330
|
+
try {
|
|
331
|
+
this.#proc.kill("SIGINT");
|
|
332
|
+
} catch (err) {
|
|
333
|
+
logger.warn("Failed to interrupt python runner", { error: err instanceof Error ? err.message : String(err) });
|
|
738
334
|
}
|
|
739
|
-
this.#pendingExecutions.clear();
|
|
740
|
-
this.#messageHandlers.clear();
|
|
741
|
-
logger.warn("Aborted pending Python executions", { reason });
|
|
742
335
|
}
|
|
743
336
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
337
|
+
async shutdown(options?: KernelShutdownOptions): Promise<KernelShutdownResult> {
|
|
338
|
+
if (this.#shutdownConfirmed) return { confirmed: true };
|
|
747
339
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
340
|
+
this.#alive = false;
|
|
341
|
+
this.#abortPendingExecutions("Python kernel shutdown");
|
|
342
|
+
|
|
343
|
+
const timeoutMs = options?.timeoutMs ?? SHUTDOWN_GRACE_MS;
|
|
344
|
+
const proc = this.#proc;
|
|
345
|
+
if (!proc) {
|
|
346
|
+
this.#shutdownConfirmed = true;
|
|
347
|
+
this.#disposed = true;
|
|
348
|
+
return { confirmed: true };
|
|
751
349
|
}
|
|
752
350
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
session: this.sessionId,
|
|
759
|
-
username: this.username,
|
|
760
|
-
date: new Date().toISOString(),
|
|
761
|
-
msg_type: "execute_request",
|
|
762
|
-
version: "5.5",
|
|
763
|
-
},
|
|
764
|
-
parent_header: {},
|
|
765
|
-
metadata: {},
|
|
766
|
-
content: {
|
|
767
|
-
code,
|
|
768
|
-
silent: options?.silent ?? false,
|
|
769
|
-
store_history: options?.storeHistory ?? !(options?.silent ?? false),
|
|
770
|
-
user_expressions: {},
|
|
771
|
-
allow_stdin: options?.allowStdin ?? false,
|
|
772
|
-
stop_on_error: true,
|
|
773
|
-
},
|
|
774
|
-
};
|
|
351
|
+
try {
|
|
352
|
+
await this.#writeLine(JSON.stringify({ type: "exit" })).catch(() => {});
|
|
353
|
+
} catch {
|
|
354
|
+
/* writer may already be closed */
|
|
355
|
+
}
|
|
775
356
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
let stdinRequested = false;
|
|
782
|
-
let cancelled = false;
|
|
783
|
-
let timedOut = false;
|
|
357
|
+
try {
|
|
358
|
+
this.#stdin?.end();
|
|
359
|
+
} catch {
|
|
360
|
+
/* ignore */
|
|
361
|
+
}
|
|
784
362
|
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
} else {
|
|
793
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
363
|
+
const exited = this.#waitForExitWithTimeout(timeoutMs);
|
|
364
|
+
let result = await exited;
|
|
365
|
+
if (!result) {
|
|
366
|
+
try {
|
|
367
|
+
proc.kill("SIGTERM");
|
|
368
|
+
} catch {
|
|
369
|
+
/* ignore */
|
|
794
370
|
}
|
|
371
|
+
result = await this.#waitForExitWithTimeout(timeoutMs);
|
|
372
|
+
}
|
|
373
|
+
if (!result) {
|
|
374
|
+
try {
|
|
375
|
+
proc.kill("SIGKILL");
|
|
376
|
+
} catch {
|
|
377
|
+
/* ignore */
|
|
378
|
+
}
|
|
379
|
+
result = await this.#waitForExitWithTimeout(timeoutMs);
|
|
795
380
|
}
|
|
796
|
-
const timeoutId =
|
|
797
|
-
typeof options?.timeoutMs === "number" && options.timeoutMs > 0
|
|
798
|
-
? setTimeout(() => {
|
|
799
|
-
timedOut = true;
|
|
800
|
-
controller.abort(new Error("Timeout"));
|
|
801
|
-
}, options.timeoutMs)
|
|
802
|
-
: undefined;
|
|
803
381
|
|
|
804
|
-
const
|
|
382
|
+
const confirmed = !!result;
|
|
383
|
+
this.#shutdownConfirmed = confirmed;
|
|
384
|
+
this.#disposed = true;
|
|
385
|
+
return { confirmed };
|
|
386
|
+
}
|
|
805
387
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
388
|
+
#abortPendingExecutions(reason: string): void {
|
|
389
|
+
if (this.#pending.size === 0) return;
|
|
390
|
+
const pending = Array.from(this.#pending.values());
|
|
391
|
+
this.#pending.clear();
|
|
392
|
+
for (const entry of pending) {
|
|
393
|
+
if (entry.settled) continue;
|
|
394
|
+
entry.settled = true;
|
|
395
|
+
void entry.options?.onChunk?.(`[kernel] ${reason}\n`);
|
|
396
|
+
entry.resolve({
|
|
397
|
+
status: "error",
|
|
398
|
+
cancelled: true,
|
|
399
|
+
timedOut: entry.timedOut,
|
|
400
|
+
stdinRequested: entry.stdinRequested,
|
|
401
|
+
executionCount: entry.executionCount,
|
|
402
|
+
error: entry.error,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
818
406
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
407
|
+
async #writeLine(line: string): Promise<void> {
|
|
408
|
+
if (!this.#stdin) {
|
|
409
|
+
throw new Error("Python kernel stdin is not open");
|
|
410
|
+
}
|
|
411
|
+
if (TRACE_IPC) {
|
|
412
|
+
logger.debug("PythonKernel send", { preview: line.slice(0, 120) });
|
|
413
|
+
}
|
|
414
|
+
this.#stdin.write(`${line}\n`);
|
|
415
|
+
this.#stdin.flush();
|
|
416
|
+
}
|
|
824
417
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
418
|
+
#startReader(stream: ReadableStream<Uint8Array>): void {
|
|
419
|
+
const reader = stream.getReader();
|
|
420
|
+
const decoder = new TextDecoder();
|
|
421
|
+
const loop = async () => {
|
|
422
|
+
try {
|
|
423
|
+
while (true) {
|
|
424
|
+
const { done, value } = await reader.read();
|
|
425
|
+
if (done) break;
|
|
426
|
+
this.#readBuffer += decoder.decode(value, { stream: true });
|
|
427
|
+
await this.#flushFrames();
|
|
428
|
+
}
|
|
429
|
+
this.#readBuffer += decoder.decode();
|
|
430
|
+
await this.#flushFrames();
|
|
431
|
+
} catch (err) {
|
|
432
|
+
logger.warn("Python kernel reader failed", { error: err instanceof Error ? err.message : String(err) });
|
|
433
|
+
} finally {
|
|
434
|
+
try {
|
|
435
|
+
reader.releaseLock();
|
|
436
|
+
} catch {
|
|
437
|
+
/* ignore */
|
|
438
|
+
}
|
|
831
439
|
}
|
|
832
|
-
finalize();
|
|
833
440
|
};
|
|
441
|
+
void loop();
|
|
442
|
+
}
|
|
834
443
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
444
|
+
#startStderrDrain(stream: ReadableStream<Uint8Array>): void {
|
|
445
|
+
// Wrapper writes its own crashes to stderr; surface them via logger so the
|
|
446
|
+
// host operator can debug runtime issues without polluting tool output.
|
|
447
|
+
const reader = stream.getReader();
|
|
448
|
+
const decoder = new TextDecoder();
|
|
449
|
+
const loop = async () => {
|
|
450
|
+
try {
|
|
451
|
+
while (true) {
|
|
452
|
+
const { done, value } = await reader.read();
|
|
453
|
+
if (done) break;
|
|
454
|
+
const text = decoder.decode(value);
|
|
455
|
+
if (text.trim()) {
|
|
456
|
+
logger.warn("Python runner stderr", { text });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} catch {
|
|
460
|
+
/* ignore */
|
|
461
|
+
} finally {
|
|
840
462
|
try {
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
|
|
463
|
+
reader.releaseLock();
|
|
464
|
+
} catch {
|
|
465
|
+
/* ignore */
|
|
844
466
|
}
|
|
845
|
-
}
|
|
467
|
+
}
|
|
846
468
|
};
|
|
847
|
-
|
|
469
|
+
void loop();
|
|
470
|
+
}
|
|
848
471
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
472
|
+
async #flushFrames(): Promise<void> {
|
|
473
|
+
while (true) {
|
|
474
|
+
const nl = this.#readBuffer.indexOf("\n");
|
|
475
|
+
if (nl < 0) return;
|
|
476
|
+
const line = this.#readBuffer.slice(0, nl);
|
|
477
|
+
this.#readBuffer = this.#readBuffer.slice(nl + 1);
|
|
478
|
+
if (!line.trim()) continue;
|
|
479
|
+
let frame: Frame;
|
|
480
|
+
try {
|
|
481
|
+
frame = JSON.parse(line) as Frame;
|
|
482
|
+
} catch (err) {
|
|
483
|
+
logger.warn("Python runner emitted invalid JSON", {
|
|
484
|
+
line: line.slice(0, 200),
|
|
485
|
+
error: err instanceof Error ? err.message : String(err),
|
|
486
|
+
});
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (TRACE_IPC) {
|
|
490
|
+
logger.debug("PythonKernel recv", { type: frame.type, id: frame.id });
|
|
491
|
+
}
|
|
492
|
+
await this.#handleFrame(frame);
|
|
852
493
|
}
|
|
494
|
+
}
|
|
853
495
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
496
|
+
async #handleFrame(frame: Frame): Promise<void> {
|
|
497
|
+
const rid = frame.id;
|
|
498
|
+
if (!rid) return;
|
|
499
|
+
const pending = this.#pending.get(rid) as (PendingExecution & { finalize?: () => void }) | undefined;
|
|
500
|
+
if (!pending) return;
|
|
501
|
+
|
|
502
|
+
switch (frame.type) {
|
|
503
|
+
case "started":
|
|
504
|
+
return;
|
|
505
|
+
case "stdout":
|
|
506
|
+
case "stderr": {
|
|
507
|
+
const text = frame.data ?? "";
|
|
508
|
+
if (text && pending.options?.onChunk) {
|
|
509
|
+
await pending.options.onChunk(text);
|
|
865
510
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
case "display":
|
|
514
|
+
case "result": {
|
|
515
|
+
const bundle = frame.bundle ?? {};
|
|
516
|
+
const { text, outputs } = await renderKernelDisplay(bundle);
|
|
517
|
+
if (text && pending.options?.onChunk) {
|
|
518
|
+
await pending.options.onChunk(text);
|
|
872
519
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if (text && options?.onChunk) {
|
|
877
|
-
await options.onChunk(text);
|
|
878
|
-
}
|
|
879
|
-
if (outputs.length > 0 && options?.onDisplay) {
|
|
880
|
-
for (const output of outputs) {
|
|
881
|
-
await options.onDisplay(output);
|
|
882
|
-
}
|
|
520
|
+
if (outputs.length > 0 && pending.options?.onDisplay) {
|
|
521
|
+
for (const output of outputs) {
|
|
522
|
+
await pending.options.onDisplay(output);
|
|
883
523
|
}
|
|
884
|
-
break;
|
|
885
524
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
case "error": {
|
|
528
|
+
const traceback = Array.isArray(frame.traceback) ? frame.traceback.map(String) : [];
|
|
529
|
+
pending.status = "error";
|
|
530
|
+
pending.error = {
|
|
531
|
+
name: String(frame.ename ?? "Error"),
|
|
532
|
+
value: String(frame.evalue ?? ""),
|
|
533
|
+
traceback,
|
|
534
|
+
};
|
|
535
|
+
const message =
|
|
536
|
+
traceback.length > 0 ? `${traceback.join("\n")}\n` : `${pending.error.name}: ${pending.error.value}\n`;
|
|
537
|
+
if (pending.options?.onChunk) {
|
|
538
|
+
await pending.options.onChunk(message);
|
|
900
539
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
}
|
|
907
|
-
break;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
case "done": {
|
|
543
|
+
if (typeof frame.executionCount === "number") {
|
|
544
|
+
pending.executionCount = frame.executionCount;
|
|
908
545
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
this.#sendMessage({
|
|
917
|
-
channel: "stdin",
|
|
918
|
-
header: {
|
|
919
|
-
msg_id: Snowflake.next(),
|
|
920
|
-
session: this.sessionId,
|
|
921
|
-
username: this.username,
|
|
922
|
-
date: new Date().toISOString(),
|
|
923
|
-
msg_type: "input_reply",
|
|
924
|
-
version: "5.5",
|
|
925
|
-
},
|
|
926
|
-
parent_header: response.header as unknown as Record<string, unknown>,
|
|
927
|
-
metadata: {},
|
|
928
|
-
content: { value: "" },
|
|
929
|
-
});
|
|
930
|
-
break;
|
|
546
|
+
if (frame.status === "error" && pending.status === "ok") {
|
|
547
|
+
pending.status = "error";
|
|
548
|
+
}
|
|
549
|
+
if (frame.cancelled) {
|
|
550
|
+
pending.cancelled = true;
|
|
931
551
|
}
|
|
552
|
+
pending.finalize?.();
|
|
553
|
+
return;
|
|
932
554
|
}
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
try {
|
|
936
|
-
this.#sendMessage(msg);
|
|
937
|
-
} catch {
|
|
938
|
-
cancelled = true;
|
|
939
|
-
finalize();
|
|
940
|
-
}
|
|
941
|
-
return promise;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
async interrupt(): Promise<void> {
|
|
945
|
-
try {
|
|
946
|
-
await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}/interrupt`, {
|
|
947
|
-
method: "POST",
|
|
948
|
-
headers: this.#authHeaders(),
|
|
949
|
-
signal: AbortSignal.timeout(2000),
|
|
950
|
-
});
|
|
951
|
-
} catch (err: unknown) {
|
|
952
|
-
logger.warn("Failed to interrupt kernel via API", { error: err instanceof Error ? err.message : String(err) });
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
try {
|
|
956
|
-
const msg: JupyterMessage = {
|
|
957
|
-
channel: "control",
|
|
958
|
-
header: {
|
|
959
|
-
msg_id: Snowflake.next(),
|
|
960
|
-
session: this.sessionId,
|
|
961
|
-
username: this.username,
|
|
962
|
-
date: new Date().toISOString(),
|
|
963
|
-
msg_type: "interrupt_request",
|
|
964
|
-
version: "5.5",
|
|
965
|
-
},
|
|
966
|
-
parent_header: {},
|
|
967
|
-
metadata: {},
|
|
968
|
-
content: {},
|
|
969
|
-
};
|
|
970
|
-
this.#sendMessage(msg);
|
|
971
|
-
} catch (err: unknown) {
|
|
972
|
-
logger.warn("Failed to send interrupt request", { error: err instanceof Error ? err.message : String(err) });
|
|
973
555
|
}
|
|
974
556
|
}
|
|
975
557
|
|
|
976
|
-
async
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
558
|
+
async #executeWithBudget(
|
|
559
|
+
code: string,
|
|
560
|
+
signal: AbortSignal | undefined,
|
|
561
|
+
timeoutMs: number,
|
|
562
|
+
label: string,
|
|
563
|
+
): Promise<void> {
|
|
564
|
+
const controller = new AbortController();
|
|
565
|
+
const cleanups: Array<() => void> = [];
|
|
566
|
+
if (signal) {
|
|
567
|
+
if (signal.aborted) {
|
|
568
|
+
controller.abort(signal.reason);
|
|
569
|
+
} else {
|
|
570
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
571
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
572
|
+
cleanups.push(() => signal.removeEventListener("abort", onAbort));
|
|
986
573
|
}
|
|
987
574
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
);
|
|
994
|
-
|
|
995
|
-
let confirmed = false;
|
|
575
|
+
const timer =
|
|
576
|
+
timeoutMs > 0
|
|
577
|
+
? setTimeout(() => controller.abort(createAbortError("TimeoutError", `${label} timed out`)), timeoutMs)
|
|
578
|
+
: undefined;
|
|
579
|
+
if (timer) cleanups.push(() => clearTimeout(timer));
|
|
996
580
|
try {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
581
|
+
throwIfAborted(controller.signal, label);
|
|
582
|
+
const result = await this.execute(code, {
|
|
583
|
+
signal: controller.signal,
|
|
584
|
+
silent: true,
|
|
585
|
+
storeHistory: false,
|
|
1001
586
|
});
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
if (!confirmed) {
|
|
1005
|
-
logger.warn("Kernel delete request was not confirmed", {
|
|
1006
|
-
status: response.status,
|
|
1007
|
-
statusText: response.statusText,
|
|
1008
|
-
});
|
|
587
|
+
if (result.cancelled) {
|
|
588
|
+
throw createAbortError(result.timedOut ? "TimeoutError" : "AbortError", `${label} cancelled`);
|
|
1009
589
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
this.#shutdownConfirmed = confirmed;
|
|
1014
|
-
this.#disposed = confirmed;
|
|
1015
|
-
|
|
1016
|
-
if (this.isSharedGateway) {
|
|
1017
|
-
try {
|
|
1018
|
-
await releaseSharedGateway();
|
|
1019
|
-
} catch (err: unknown) {
|
|
1020
|
-
logger.warn("Failed to release shared gateway after kernel shutdown", {
|
|
1021
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1022
|
-
});
|
|
590
|
+
if (result.status === "error") {
|
|
591
|
+
const reason = result.error?.value ?? "Python kernel init failed";
|
|
592
|
+
throw new Error(`${label} failed: ${reason}`);
|
|
1023
593
|
}
|
|
594
|
+
} finally {
|
|
595
|
+
for (const cleanup of cleanups) cleanup();
|
|
1024
596
|
}
|
|
1025
|
-
|
|
1026
|
-
return { confirmed };
|
|
1027
597
|
}
|
|
1028
598
|
|
|
1029
|
-
#
|
|
1030
|
-
if (!this.#
|
|
1031
|
-
|
|
1032
|
-
|
|
599
|
+
#waitForExitWithTimeout(timeoutMs: number): Promise<number | null> {
|
|
600
|
+
if (!this.#exitedPromise) return Promise.resolve(0);
|
|
601
|
+
const exitedPromise = this.#exitedPromise;
|
|
602
|
+
const timeout = new Promise<null>(resolve => {
|
|
603
|
+
const timer = setTimeout(() => resolve(null), Math.max(0, timeoutMs));
|
|
604
|
+
(timer as { unref?: () => void }).unref?.();
|
|
605
|
+
});
|
|
606
|
+
return Promise.race([exitedPromise.then(code => code as number | null), timeout]);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
1033
609
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
610
|
+
function isTimeoutReason(reason: unknown): boolean {
|
|
611
|
+
if (reason instanceof DOMException) return reason.name === "TimeoutError";
|
|
612
|
+
if (reason instanceof Error) return reason.name === "TimeoutError";
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
1041
615
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
this.#ws.send(JSON.stringify(payload));
|
|
1054
|
-
}
|
|
616
|
+
function buildInitScript(cwd: string, env?: Record<string, string | undefined>): string {
|
|
617
|
+
const envEntries = Object.entries(env ?? {}).filter(([, value]) => value !== undefined);
|
|
618
|
+
const envPayload = Object.fromEntries(envEntries);
|
|
619
|
+
return [
|
|
620
|
+
"import os, sys",
|
|
621
|
+
`__omp_cwd = ${JSON.stringify(cwd)}`,
|
|
622
|
+
"os.chdir(__omp_cwd)",
|
|
623
|
+
`__omp_env = ${JSON.stringify(envPayload)}`,
|
|
624
|
+
"for __omp_key, __omp_val in __omp_env.items():\n os.environ[__omp_key] = __omp_val",
|
|
625
|
+
"if __omp_cwd not in sys.path:\n sys.path.insert(0, __omp_cwd)",
|
|
626
|
+
].join("\n");
|
|
1055
627
|
}
|