@cloudflare/sandbox 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/Dockerfile +37 -11
- package/README.md +229 -5
- package/container_src/bun.lock +122 -0
- package/container_src/handler/exec.ts +4 -2
- package/container_src/handler/process.ts +1 -1
- package/container_src/index.ts +171 -1
- package/container_src/jupyter-server.ts +336 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/startup.sh +52 -0
- package/dist/{chunk-YVZ3K26G.js → chunk-CUHYLCMT.js} +9 -21
- package/dist/chunk-CUHYLCMT.js.map +1 -0
- package/dist/chunk-EGC5IYXA.js +108 -0
- package/dist/chunk-EGC5IYXA.js.map +1 -0
- package/dist/chunk-FKBV7CZS.js +113 -0
- package/dist/chunk-FKBV7CZS.js.map +1 -0
- package/dist/{chunk-ZJN2PQOS.js → chunk-IATLC32Y.js} +173 -74
- package/dist/chunk-IATLC32Y.js.map +1 -0
- package/dist/{chunk-6THNBO4S.js → chunk-S5FFBU4Y.js} +1 -1
- package/dist/{chunk-6THNBO4S.js.map → chunk-S5FFBU4Y.js.map} +1 -1
- package/dist/chunk-SYMWNYWA.js +185 -0
- package/dist/chunk-SYMWNYWA.js.map +1 -0
- package/dist/{client-BXYlxy-j.d.ts → client-C7rKCYBD.d.ts} +42 -4
- package/dist/client.d.ts +2 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +10 -4
- package/dist/interpreter-types.d.ts +259 -0
- package/dist/interpreter-types.js +9 -0
- package/dist/interpreter-types.js.map +1 -0
- package/dist/interpreter.d.ts +33 -0
- package/dist/interpreter.js +8 -0
- package/dist/interpreter.js.map +1 -0
- package/dist/jupyter-client.d.ts +4 -0
- package/dist/jupyter-client.js +8 -0
- package/dist/jupyter-client.js.map +1 -0
- package/dist/request-handler.d.ts +2 -1
- package/dist/request-handler.js +7 -3
- package/dist/sandbox.d.ts +2 -1
- package/dist/sandbox.js +7 -3
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/src/client.ts +37 -54
- package/src/index.ts +13 -4
- package/src/interpreter-types.ts +383 -0
- package/src/interpreter.ts +150 -0
- package/src/jupyter-client.ts +266 -0
- package/src/sandbox.ts +281 -153
- package/src/types.ts +15 -0
- package/dist/chunk-YVZ3K26G.js.map +0 -1
- package/dist/chunk-ZJN2PQOS.js.map +0 -1
package/container_src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
handleStartProcessRequest,
|
|
26
26
|
handleStreamProcessLogsRequest,
|
|
27
27
|
} from "./handler/process";
|
|
28
|
+
import { type CreateContextRequest, JupyterServer } from "./jupyter-server";
|
|
28
29
|
import type { ProcessRecord, SessionData } from "./types";
|
|
29
30
|
|
|
30
31
|
// In-memory session storage (in production, you'd want to use a proper database)
|
|
@@ -55,8 +56,28 @@ function cleanupOldSessions() {
|
|
|
55
56
|
// Run cleanup every 10 minutes
|
|
56
57
|
setInterval(cleanupOldSessions, 10 * 60 * 1000);
|
|
57
58
|
|
|
59
|
+
// Initialize Jupyter server
|
|
60
|
+
const jupyterServer = new JupyterServer();
|
|
61
|
+
let jupyterInitialized = false;
|
|
62
|
+
|
|
63
|
+
// Initialize Jupyter immediately since startup.sh ensures it's ready
|
|
64
|
+
(async () => {
|
|
65
|
+
try {
|
|
66
|
+
await jupyterServer.initialize();
|
|
67
|
+
jupyterInitialized = true;
|
|
68
|
+
console.log("[Container] Jupyter integration initialized successfully");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("[Container] Failed to initialize Jupyter:", error);
|
|
71
|
+
// Log more details to help debug
|
|
72
|
+
if (error instanceof Error) {
|
|
73
|
+
console.error("[Container] Error details:", error.message);
|
|
74
|
+
console.error("[Container] Stack trace:", error.stack);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
|
|
58
79
|
const server = serve({
|
|
59
|
-
fetch(req: Request) {
|
|
80
|
+
async fetch(req: Request) {
|
|
60
81
|
const url = new URL(req.url);
|
|
61
82
|
const pathname = url.pathname;
|
|
62
83
|
|
|
@@ -159,6 +180,7 @@ const server = serve({
|
|
|
159
180
|
JSON.stringify({
|
|
160
181
|
message: "pong",
|
|
161
182
|
timestamp: new Date().toISOString(),
|
|
183
|
+
jupyter: jupyterInitialized ? "ready" : "not ready",
|
|
162
184
|
}),
|
|
163
185
|
{
|
|
164
186
|
headers: {
|
|
@@ -280,7 +302,151 @@ const server = serve({
|
|
|
280
302
|
}
|
|
281
303
|
break;
|
|
282
304
|
|
|
305
|
+
// Code interpreter endpoints
|
|
306
|
+
case "/api/contexts":
|
|
307
|
+
if (req.method === "POST") {
|
|
308
|
+
if (!jupyterInitialized) {
|
|
309
|
+
return new Response(
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
error: "Jupyter server is not ready. Please try again in a moment."
|
|
312
|
+
}),
|
|
313
|
+
{
|
|
314
|
+
status: 503,
|
|
315
|
+
headers: {
|
|
316
|
+
"Content-Type": "application/json",
|
|
317
|
+
...corsHeaders,
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const body = await req.json() as CreateContextRequest;
|
|
324
|
+
const context = await jupyterServer.createContext(body);
|
|
325
|
+
return new Response(
|
|
326
|
+
JSON.stringify({
|
|
327
|
+
id: context.id,
|
|
328
|
+
language: context.language,
|
|
329
|
+
cwd: context.cwd,
|
|
330
|
+
createdAt: context.createdAt,
|
|
331
|
+
lastUsed: context.lastUsed
|
|
332
|
+
}),
|
|
333
|
+
{
|
|
334
|
+
headers: {
|
|
335
|
+
"Content-Type": "application/json",
|
|
336
|
+
...corsHeaders,
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error("[Container] Error creating context:", error);
|
|
342
|
+
return new Response(
|
|
343
|
+
JSON.stringify({
|
|
344
|
+
error: error instanceof Error ? error.message : "Failed to create context"
|
|
345
|
+
}),
|
|
346
|
+
{
|
|
347
|
+
status: 500,
|
|
348
|
+
headers: {
|
|
349
|
+
"Content-Type": "application/json",
|
|
350
|
+
...corsHeaders,
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
} else if (req.method === "GET") {
|
|
356
|
+
if (!jupyterInitialized) {
|
|
357
|
+
return new Response(
|
|
358
|
+
JSON.stringify({ contexts: [] }),
|
|
359
|
+
{
|
|
360
|
+
headers: {
|
|
361
|
+
"Content-Type": "application/json",
|
|
362
|
+
...corsHeaders,
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const contexts = await jupyterServer.listContexts();
|
|
368
|
+
return new Response(
|
|
369
|
+
JSON.stringify({ contexts }),
|
|
370
|
+
{
|
|
371
|
+
headers: {
|
|
372
|
+
"Content-Type": "application/json",
|
|
373
|
+
...corsHeaders,
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
|
|
380
|
+
case "/api/execute/code":
|
|
381
|
+
if (req.method === "POST") {
|
|
382
|
+
if (!jupyterInitialized) {
|
|
383
|
+
return new Response(
|
|
384
|
+
JSON.stringify({
|
|
385
|
+
error: "Jupyter server is not ready. Please try again in a moment."
|
|
386
|
+
}),
|
|
387
|
+
{
|
|
388
|
+
status: 503,
|
|
389
|
+
headers: {
|
|
390
|
+
"Content-Type": "application/json",
|
|
391
|
+
...corsHeaders,
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const body = await req.json() as { context_id: string; code: string; language?: string };
|
|
398
|
+
return await jupyterServer.executeCode(body.context_id, body.code, body.language);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error("[Container] Error executing code:", error);
|
|
401
|
+
return new Response(
|
|
402
|
+
JSON.stringify({
|
|
403
|
+
error: error instanceof Error ? error.message : "Failed to execute code"
|
|
404
|
+
}),
|
|
405
|
+
{
|
|
406
|
+
status: 500,
|
|
407
|
+
headers: {
|
|
408
|
+
"Content-Type": "application/json",
|
|
409
|
+
...corsHeaders,
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
|
|
283
417
|
default:
|
|
418
|
+
// Handle dynamic routes for contexts
|
|
419
|
+
if (pathname.startsWith("/api/contexts/") && pathname.split('/').length === 4) {
|
|
420
|
+
const contextId = pathname.split('/')[3];
|
|
421
|
+
if (req.method === "DELETE") {
|
|
422
|
+
try {
|
|
423
|
+
await jupyterServer.deleteContext(contextId);
|
|
424
|
+
return new Response(
|
|
425
|
+
JSON.stringify({ success: true }),
|
|
426
|
+
{
|
|
427
|
+
headers: {
|
|
428
|
+
"Content-Type": "application/json",
|
|
429
|
+
...corsHeaders,
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
return new Response(
|
|
435
|
+
JSON.stringify({
|
|
436
|
+
error: error instanceof Error ? error.message : "Failed to delete context"
|
|
437
|
+
}),
|
|
438
|
+
{
|
|
439
|
+
status: error instanceof Error && error.message.includes("not found") ? 404 : 500,
|
|
440
|
+
headers: {
|
|
441
|
+
"Content-Type": "application/json",
|
|
442
|
+
...corsHeaders,
|
|
443
|
+
},
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
284
450
|
// Handle dynamic routes for individual processes
|
|
285
451
|
if (pathname.startsWith("/api/process/")) {
|
|
286
452
|
const segments = pathname.split('/');
|
|
@@ -357,5 +523,9 @@ console.log(` GET /api/process/{id}/logs - Get process logs`);
|
|
|
357
523
|
console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
|
|
358
524
|
console.log(` DELETE /api/process/kill-all - Kill all processes`);
|
|
359
525
|
console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
|
|
526
|
+
console.log(` POST /api/contexts - Create a code execution context`);
|
|
527
|
+
console.log(` GET /api/contexts - List all contexts`);
|
|
528
|
+
console.log(` DELETE /api/contexts/{id} - Delete a context`);
|
|
529
|
+
console.log(` POST /api/execute/code - Execute code in a context (streaming)`);
|
|
360
530
|
console.log(` GET /api/ping - Health check`);
|
|
361
531
|
console.log(` GET /api/commands - List available commands`);
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { type Kernel, KernelManager, ServerConnection } from "@jupyterlab/services";
|
|
2
|
+
import type {
|
|
3
|
+
IDisplayDataMsg,
|
|
4
|
+
IErrorMsg,
|
|
5
|
+
IExecuteResultMsg,
|
|
6
|
+
IIOPubMessage,
|
|
7
|
+
IStreamMsg
|
|
8
|
+
} from "@jupyterlab/services/lib/kernel/messages";
|
|
9
|
+
import {
|
|
10
|
+
isDisplayDataMsg,
|
|
11
|
+
isErrorMsg,
|
|
12
|
+
isExecuteResultMsg,
|
|
13
|
+
isStreamMsg
|
|
14
|
+
} from "@jupyterlab/services/lib/kernel/messages";
|
|
15
|
+
import { v4 as uuidv4 } from "uuid";
|
|
16
|
+
import type { ExecutionResult } from "./mime-processor";
|
|
17
|
+
import { processJupyterMessage } from "./mime-processor";
|
|
18
|
+
|
|
19
|
+
export interface JupyterContext {
|
|
20
|
+
id: string;
|
|
21
|
+
language: string;
|
|
22
|
+
connection: Kernel.IKernelConnection;
|
|
23
|
+
cwd: string;
|
|
24
|
+
createdAt: Date;
|
|
25
|
+
lastUsed: Date;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CreateContextRequest {
|
|
29
|
+
language?: string;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
envVars?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ExecuteCodeRequest {
|
|
35
|
+
context_id?: string;
|
|
36
|
+
code: string;
|
|
37
|
+
language?: string;
|
|
38
|
+
env_vars?: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class JupyterServer {
|
|
42
|
+
private kernelManager: KernelManager;
|
|
43
|
+
private contexts = new Map<string, JupyterContext>();
|
|
44
|
+
private defaultContexts = new Map<string, string>(); // language -> context_id
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
// Configure connection to local Jupyter server
|
|
48
|
+
const serverSettings = ServerConnection.makeSettings({
|
|
49
|
+
baseUrl: "http://localhost:8888",
|
|
50
|
+
token: "",
|
|
51
|
+
appUrl: "",
|
|
52
|
+
wsUrl: "ws://localhost:8888",
|
|
53
|
+
appendToken: false,
|
|
54
|
+
init: {
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.kernelManager = new KernelManager({ serverSettings });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async initialize() {
|
|
65
|
+
await this.kernelManager.ready;
|
|
66
|
+
console.log("[JupyterServer] Kernel manager initialized");
|
|
67
|
+
|
|
68
|
+
// Create default Python context
|
|
69
|
+
const pythonContext = await this.createContext({ language: "python" });
|
|
70
|
+
this.defaultContexts.set("python", pythonContext.id);
|
|
71
|
+
console.log(
|
|
72
|
+
"[JupyterServer] Default Python context created:",
|
|
73
|
+
pythonContext.id
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async createContext(req: CreateContextRequest): Promise<JupyterContext> {
|
|
78
|
+
const language = req.language || "python";
|
|
79
|
+
const cwd = req.cwd || "/workspace";
|
|
80
|
+
|
|
81
|
+
const kernelModel = await this.kernelManager.startNew({
|
|
82
|
+
name: this.getKernelName(language),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const connection = this.kernelManager.connectTo({ model: kernelModel });
|
|
86
|
+
|
|
87
|
+
const context: JupyterContext = {
|
|
88
|
+
id: uuidv4(),
|
|
89
|
+
language,
|
|
90
|
+
connection,
|
|
91
|
+
cwd,
|
|
92
|
+
createdAt: new Date(),
|
|
93
|
+
lastUsed: new Date(),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
this.contexts.set(context.id, context);
|
|
97
|
+
|
|
98
|
+
// Set working directory
|
|
99
|
+
if (cwd !== "/workspace") {
|
|
100
|
+
await this.changeWorkingDirectory(context, cwd);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Set environment variables if provided
|
|
104
|
+
if (req.envVars) {
|
|
105
|
+
await this.setEnvironmentVariables(context, req.envVars);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return context;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async executeCode(
|
|
112
|
+
contextId: string | undefined,
|
|
113
|
+
code: string,
|
|
114
|
+
language?: string
|
|
115
|
+
): Promise<Response> {
|
|
116
|
+
let context: JupyterContext | undefined;
|
|
117
|
+
|
|
118
|
+
if (contextId) {
|
|
119
|
+
context = this.contexts.get(contextId);
|
|
120
|
+
if (!context) {
|
|
121
|
+
return new Response(
|
|
122
|
+
JSON.stringify({ error: `Context ${contextId} not found` }),
|
|
123
|
+
{
|
|
124
|
+
status: 404,
|
|
125
|
+
headers: { "Content-Type": "application/json" },
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
} else if (language) {
|
|
130
|
+
// Use default context for the language
|
|
131
|
+
const defaultContextId = this.defaultContexts.get(language);
|
|
132
|
+
if (defaultContextId) {
|
|
133
|
+
context = this.contexts.get(defaultContextId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create new default context if needed
|
|
137
|
+
if (!context) {
|
|
138
|
+
context = await this.createContext({ language });
|
|
139
|
+
this.defaultContexts.set(language, context.id);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// Use default Python context
|
|
143
|
+
const pythonContextId = this.defaultContexts.get("python");
|
|
144
|
+
context = pythonContextId
|
|
145
|
+
? this.contexts.get(pythonContextId)
|
|
146
|
+
: undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!context) {
|
|
150
|
+
return new Response(JSON.stringify({ error: "No context available" }), {
|
|
151
|
+
status: 400,
|
|
152
|
+
headers: { "Content-Type": "application/json" },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Update last used
|
|
157
|
+
context.lastUsed = new Date();
|
|
158
|
+
|
|
159
|
+
// Execute with streaming
|
|
160
|
+
return this.streamExecution(context.connection, code);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async streamExecution(
|
|
164
|
+
connection: Kernel.IKernelConnection,
|
|
165
|
+
code: string
|
|
166
|
+
): Promise<Response> {
|
|
167
|
+
const stream = new ReadableStream({
|
|
168
|
+
async start(controller) {
|
|
169
|
+
const future = connection.requestExecute({
|
|
170
|
+
code,
|
|
171
|
+
stop_on_error: false,
|
|
172
|
+
store_history: true,
|
|
173
|
+
silent: false,
|
|
174
|
+
allow_stdin: false,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Handle different message types
|
|
178
|
+
future.onIOPub = (msg: IIOPubMessage) => {
|
|
179
|
+
const result = processJupyterMessage(msg);
|
|
180
|
+
if (result) {
|
|
181
|
+
controller.enqueue(
|
|
182
|
+
new TextEncoder().encode(`${JSON.stringify(result)}\n`)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
future.onReply = (msg: any) => {
|
|
188
|
+
if (msg.content.status === "ok") {
|
|
189
|
+
controller.enqueue(
|
|
190
|
+
new TextEncoder().encode(
|
|
191
|
+
`${JSON.stringify({
|
|
192
|
+
type: "execution_complete",
|
|
193
|
+
execution_count: msg.content.execution_count,
|
|
194
|
+
})}\n`
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
} else if (msg.content.status === "error") {
|
|
198
|
+
controller.enqueue(
|
|
199
|
+
new TextEncoder().encode(
|
|
200
|
+
`${JSON.stringify({
|
|
201
|
+
type: "error",
|
|
202
|
+
ename: msg.content.ename,
|
|
203
|
+
evalue: msg.content.evalue,
|
|
204
|
+
traceback: msg.content.traceback,
|
|
205
|
+
})}\n`
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
controller.close();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
future.onStdin = (msg: any) => {
|
|
213
|
+
// We don't support stdin for now
|
|
214
|
+
console.warn("[JupyterServer] Stdin requested but not supported");
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return new Response(stream, {
|
|
220
|
+
headers: {
|
|
221
|
+
"Content-Type": "text/event-stream",
|
|
222
|
+
"Cache-Control": "no-cache",
|
|
223
|
+
Connection: "keep-alive",
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private getKernelName(language: string): string {
|
|
229
|
+
const kernelMap: Record<string, string> = {
|
|
230
|
+
python: "python3",
|
|
231
|
+
javascript: "javascript",
|
|
232
|
+
typescript: "javascript",
|
|
233
|
+
js: "javascript",
|
|
234
|
+
ts: "javascript",
|
|
235
|
+
};
|
|
236
|
+
return kernelMap[language.toLowerCase()] || "python3";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async changeWorkingDirectory(context: JupyterContext, cwd: string) {
|
|
240
|
+
const code =
|
|
241
|
+
context.language === "python"
|
|
242
|
+
? `import os; os.chdir('${cwd}')`
|
|
243
|
+
: `process.chdir('${cwd}')`;
|
|
244
|
+
|
|
245
|
+
const future = context.connection.requestExecute({
|
|
246
|
+
code,
|
|
247
|
+
silent: true,
|
|
248
|
+
store_history: false,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return future.done;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private async setEnvironmentVariables(
|
|
255
|
+
context: JupyterContext,
|
|
256
|
+
envVars: Record<string, string>
|
|
257
|
+
) {
|
|
258
|
+
const commands: string[] = [];
|
|
259
|
+
|
|
260
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
261
|
+
if (context.language === "python") {
|
|
262
|
+
commands.push(`import os; os.environ['${key}'] = '${value}'`);
|
|
263
|
+
} else if (
|
|
264
|
+
context.language === "javascript" ||
|
|
265
|
+
context.language === "typescript"
|
|
266
|
+
) {
|
|
267
|
+
commands.push(`process.env['${key}'] = '${value}'`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (commands.length > 0) {
|
|
272
|
+
const code = commands.join("\n");
|
|
273
|
+
const future = context.connection.requestExecute({
|
|
274
|
+
code,
|
|
275
|
+
silent: true,
|
|
276
|
+
store_history: false,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return future.done;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async listContexts(): Promise<
|
|
284
|
+
Array<{
|
|
285
|
+
id: string;
|
|
286
|
+
language: string;
|
|
287
|
+
cwd: string;
|
|
288
|
+
createdAt: Date;
|
|
289
|
+
lastUsed: Date;
|
|
290
|
+
}>
|
|
291
|
+
> {
|
|
292
|
+
return Array.from(this.contexts.values()).map((ctx) => ({
|
|
293
|
+
id: ctx.id,
|
|
294
|
+
language: ctx.language,
|
|
295
|
+
cwd: ctx.cwd,
|
|
296
|
+
createdAt: ctx.createdAt,
|
|
297
|
+
lastUsed: ctx.lastUsed,
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async deleteContext(contextId: string): Promise<void> {
|
|
302
|
+
const context = this.contexts.get(contextId);
|
|
303
|
+
if (!context) {
|
|
304
|
+
throw new Error(`Context ${contextId} not found`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Shutdown the kernel
|
|
308
|
+
await context.connection.shutdown();
|
|
309
|
+
|
|
310
|
+
// Remove from maps
|
|
311
|
+
this.contexts.delete(contextId);
|
|
312
|
+
|
|
313
|
+
// Remove from default contexts if it was a default
|
|
314
|
+
for (const [lang, id] of this.defaultContexts.entries()) {
|
|
315
|
+
if (id === contextId) {
|
|
316
|
+
this.defaultContexts.delete(lang);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async shutdown() {
|
|
323
|
+
// Shutdown all kernels
|
|
324
|
+
for (const context of this.contexts.values()) {
|
|
325
|
+
try {
|
|
326
|
+
await context.connection.shutdown();
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error("[JupyterServer] Error shutting down kernel:", error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.contexts.clear();
|
|
333
|
+
this.defaultContexts.clear();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|