@cloudflare/sandbox 0.2.0 → 0.2.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/CHANGELOG.md +12 -0
- package/Dockerfile +31 -7
- package/README.md +226 -2
- package/container_src/bun.lock +122 -0
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/index.ts +305 -10
- package/container_src/jupyter-server.ts +579 -0
- package/container_src/jupyter-service.ts +448 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/startup.sh +83 -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-LALY4SFU.js +129 -0
- package/dist/chunk-LALY4SFU.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-VTKZL632.js +237 -0
- package/dist/chunk-VTKZL632.js.map +1 -0
- package/dist/{chunk-ZJN2PQOS.js → chunk-ZMPO44U4.js} +171 -72
- package/dist/chunk-ZMPO44U4.js.map +1 -0
- package/dist/{client-BXYlxy-j.d.ts → client-bzEV222a.d.ts} +52 -4
- package/dist/client.d.ts +2 -1
- package/dist/client.js +1 -1
- package/dist/errors.d.ts +95 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +33 -3
- 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 +9 -0
- package/dist/jupyter-client.js.map +1 -0
- package/dist/request-handler.d.ts +2 -1
- package/dist/request-handler.js +8 -3
- package/dist/sandbox.d.ts +2 -1
- package/dist/sandbox.js +8 -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/errors.ts +218 -0
- package/src/index.ts +44 -10
- package/src/interpreter-types.ts +383 -0
- package/src/interpreter.ts +150 -0
- package/src/jupyter-client.ts +349 -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
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Kernel,
|
|
3
|
+
KernelManager,
|
|
4
|
+
ServerConnection,
|
|
5
|
+
} from "@jupyterlab/services";
|
|
6
|
+
import type { IIOPubMessage } from "@jupyterlab/services/lib/kernel/messages";
|
|
7
|
+
import { v4 as uuidv4 } from "uuid";
|
|
8
|
+
import { processJupyterMessage } from "./mime-processor";
|
|
9
|
+
|
|
10
|
+
export interface JupyterContext {
|
|
11
|
+
id: string;
|
|
12
|
+
language: string;
|
|
13
|
+
connection: Kernel.IKernelConnection;
|
|
14
|
+
cwd: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
lastUsed: Date;
|
|
17
|
+
pooled?: boolean; // Track if this context is from the pool
|
|
18
|
+
inUse?: boolean; // Track if pooled context is currently in use
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CreateContextRequest {
|
|
22
|
+
language?: string;
|
|
23
|
+
cwd?: string;
|
|
24
|
+
envVars?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ExecuteCodeRequest {
|
|
28
|
+
context_id?: string;
|
|
29
|
+
code: string;
|
|
30
|
+
language?: string;
|
|
31
|
+
env_vars?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ContextPool {
|
|
35
|
+
available: JupyterContext[];
|
|
36
|
+
inUse: Set<string>; // context IDs currently in use
|
|
37
|
+
minSize: number;
|
|
38
|
+
maxSize: number;
|
|
39
|
+
warming: boolean; // Track if pool is currently being warmed
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class JupyterServer {
|
|
43
|
+
private kernelManager: KernelManager;
|
|
44
|
+
private contexts = new Map<string, JupyterContext>();
|
|
45
|
+
private defaultContexts = new Map<string, string>(); // language -> context_id
|
|
46
|
+
private contextPools = new Map<string, ContextPool>(); // language -> pool
|
|
47
|
+
|
|
48
|
+
constructor() {
|
|
49
|
+
// Configure connection to local Jupyter server
|
|
50
|
+
const serverSettings = ServerConnection.makeSettings({
|
|
51
|
+
baseUrl: "http://localhost:8888",
|
|
52
|
+
token: "",
|
|
53
|
+
appUrl: "",
|
|
54
|
+
wsUrl: "ws://localhost:8888",
|
|
55
|
+
appendToken: false,
|
|
56
|
+
init: {
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.kernelManager = new KernelManager({ serverSettings });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async initialize() {
|
|
67
|
+
await this.kernelManager.ready;
|
|
68
|
+
console.log("[JupyterServer] Kernel manager initialized");
|
|
69
|
+
|
|
70
|
+
// Don't create default context during initialization - use lazy loading instead
|
|
71
|
+
|
|
72
|
+
// Initialize pools for common languages (but don't warm them yet)
|
|
73
|
+
this.initializePool("python", 0, 3);
|
|
74
|
+
this.initializePool("javascript", 0, 2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Initialize a context pool for a specific language
|
|
79
|
+
*/
|
|
80
|
+
private initializePool(language: string, minSize = 0, maxSize = 5) {
|
|
81
|
+
const pool: ContextPool = {
|
|
82
|
+
available: [],
|
|
83
|
+
inUse: new Set(),
|
|
84
|
+
minSize,
|
|
85
|
+
maxSize,
|
|
86
|
+
warming: false,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.contextPools.set(language, pool);
|
|
90
|
+
|
|
91
|
+
// Pre-warm contexts in background if minSize > 0
|
|
92
|
+
if (minSize > 0) {
|
|
93
|
+
setTimeout(() => this.warmPool(language, minSize), 0);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Enable pool warming for a language (called after Jupyter is ready)
|
|
99
|
+
*/
|
|
100
|
+
async enablePoolWarming(language: string, minSize: number) {
|
|
101
|
+
const pool = this.contextPools.get(language);
|
|
102
|
+
if (!pool) {
|
|
103
|
+
this.initializePool(language, minSize, 3);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Update min size and warm if needed
|
|
108
|
+
pool.minSize = minSize;
|
|
109
|
+
const toWarm = minSize - pool.available.length;
|
|
110
|
+
if (toWarm > 0) {
|
|
111
|
+
await this.warmPool(language, toWarm);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Pre-warm a pool with the specified number of contexts
|
|
117
|
+
*/
|
|
118
|
+
private async warmPool(language: string, count: number) {
|
|
119
|
+
const pool = this.contextPools.get(language);
|
|
120
|
+
if (!pool || pool.warming) return;
|
|
121
|
+
|
|
122
|
+
pool.warming = true;
|
|
123
|
+
console.log(`[JupyterServer] Pre-warming ${count} ${language} contexts`);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const promises: Promise<JupyterContext>[] = [];
|
|
127
|
+
for (let i = 0; i < count; i++) {
|
|
128
|
+
promises.push(this.createPooledContext(language));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const contexts = await Promise.all(promises);
|
|
132
|
+
pool.available.push(...contexts);
|
|
133
|
+
console.log(
|
|
134
|
+
`[JupyterServer] Pre-warmed ${contexts.length} ${language} contexts`
|
|
135
|
+
);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(
|
|
138
|
+
`[JupyterServer] Error pre-warming ${language} pool:`,
|
|
139
|
+
error
|
|
140
|
+
);
|
|
141
|
+
} finally {
|
|
142
|
+
pool.warming = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a context specifically for the pool
|
|
148
|
+
*/
|
|
149
|
+
private async createPooledContext(language: string): Promise<JupyterContext> {
|
|
150
|
+
const context = await this.createContext({ language });
|
|
151
|
+
context.pooled = true;
|
|
152
|
+
context.inUse = false;
|
|
153
|
+
return context;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get a context from the pool or create a new one
|
|
158
|
+
*/
|
|
159
|
+
private async getPooledContext(
|
|
160
|
+
language: string,
|
|
161
|
+
cwd?: string,
|
|
162
|
+
envVars?: Record<string, string>
|
|
163
|
+
): Promise<JupyterContext | null> {
|
|
164
|
+
const pool = this.contextPools.get(language);
|
|
165
|
+
if (!pool) return null;
|
|
166
|
+
|
|
167
|
+
// Find an available context in the pool
|
|
168
|
+
const availableContext = pool.available.find((ctx) => !ctx.inUse);
|
|
169
|
+
|
|
170
|
+
if (availableContext) {
|
|
171
|
+
// Mark as in use
|
|
172
|
+
availableContext.inUse = true;
|
|
173
|
+
pool.inUse.add(availableContext.id);
|
|
174
|
+
|
|
175
|
+
// Remove from available list
|
|
176
|
+
pool.available = pool.available.filter(
|
|
177
|
+
(ctx) => ctx.id !== availableContext.id
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Update context properties if needed
|
|
181
|
+
if (cwd && cwd !== availableContext.cwd) {
|
|
182
|
+
await this.changeWorkingDirectory(availableContext, cwd);
|
|
183
|
+
availableContext.cwd = cwd;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (envVars) {
|
|
187
|
+
await this.setEnvironmentVariables(availableContext, envVars);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
availableContext.lastUsed = new Date();
|
|
191
|
+
console.log(
|
|
192
|
+
`[JupyterServer] Reusing pooled ${language} context ${availableContext.id}`
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Warm another context in background if we're below minSize
|
|
196
|
+
if (pool.available.length < pool.minSize && !pool.warming) {
|
|
197
|
+
setTimeout(() => this.warmPool(language, 1), 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return availableContext;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// No available context, check if we can create a new one
|
|
204
|
+
if (pool.inUse.size < pool.maxSize) {
|
|
205
|
+
console.log(
|
|
206
|
+
`[JupyterServer] No pooled ${language} context available, creating new one`
|
|
207
|
+
);
|
|
208
|
+
return null; // Let the caller create a new context
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Pool is at max capacity
|
|
212
|
+
console.log(
|
|
213
|
+
`[JupyterServer] ${language} context pool at max capacity (${pool.maxSize})`
|
|
214
|
+
);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Release a pooled context back to the pool
|
|
220
|
+
*/
|
|
221
|
+
private releasePooledContext(context: JupyterContext) {
|
|
222
|
+
if (!context.pooled) return;
|
|
223
|
+
|
|
224
|
+
const pool = this.contextPools.get(context.language);
|
|
225
|
+
if (!pool) return;
|
|
226
|
+
|
|
227
|
+
// Mark as not in use
|
|
228
|
+
context.inUse = false;
|
|
229
|
+
pool.inUse.delete(context.id);
|
|
230
|
+
|
|
231
|
+
// Add back to available list
|
|
232
|
+
pool.available.push(context);
|
|
233
|
+
|
|
234
|
+
console.log(
|
|
235
|
+
`[JupyterServer] Released ${context.language} context ${context.id} back to pool`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async createContext(req: CreateContextRequest): Promise<JupyterContext> {
|
|
240
|
+
const language = req.language || "python";
|
|
241
|
+
const cwd = req.cwd || "/workspace";
|
|
242
|
+
|
|
243
|
+
// Try to get a context from the pool first
|
|
244
|
+
const pooledContext = await this.getPooledContext(
|
|
245
|
+
language,
|
|
246
|
+
cwd,
|
|
247
|
+
req.envVars
|
|
248
|
+
);
|
|
249
|
+
if (pooledContext) {
|
|
250
|
+
// Add to active contexts map
|
|
251
|
+
this.contexts.set(pooledContext.id, pooledContext);
|
|
252
|
+
return pooledContext;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Create a new context if pool didn't provide one
|
|
256
|
+
const kernelModel = await this.kernelManager.startNew({
|
|
257
|
+
name: this.getKernelName(language),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const connection = this.kernelManager.connectTo({ model: kernelModel });
|
|
261
|
+
|
|
262
|
+
const context: JupyterContext = {
|
|
263
|
+
id: uuidv4(),
|
|
264
|
+
language,
|
|
265
|
+
connection,
|
|
266
|
+
cwd,
|
|
267
|
+
createdAt: new Date(),
|
|
268
|
+
lastUsed: new Date(),
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
this.contexts.set(context.id, context);
|
|
272
|
+
|
|
273
|
+
// Set working directory
|
|
274
|
+
if (cwd !== "/workspace") {
|
|
275
|
+
await this.changeWorkingDirectory(context, cwd);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Set environment variables if provided
|
|
279
|
+
if (req.envVars) {
|
|
280
|
+
await this.setEnvironmentVariables(context, req.envVars);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return context;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async getOrCreateDefaultContext(
|
|
287
|
+
language: string
|
|
288
|
+
): Promise<JupyterContext | undefined> {
|
|
289
|
+
// Check if we already have a default context for this language
|
|
290
|
+
const defaultContextId = this.defaultContexts.get(language);
|
|
291
|
+
if (defaultContextId) {
|
|
292
|
+
const context = this.contexts.get(defaultContextId);
|
|
293
|
+
if (context) {
|
|
294
|
+
return context;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create new default context lazily
|
|
299
|
+
console.log(
|
|
300
|
+
`[JupyterServer] Creating default ${language} context on first use`
|
|
301
|
+
);
|
|
302
|
+
const context = await this.createContext({ language });
|
|
303
|
+
this.defaultContexts.set(language, context.id);
|
|
304
|
+
return context;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async executeCode(
|
|
308
|
+
contextId: string | undefined,
|
|
309
|
+
code: string,
|
|
310
|
+
language?: string
|
|
311
|
+
): Promise<Response> {
|
|
312
|
+
let context: JupyterContext | undefined;
|
|
313
|
+
|
|
314
|
+
if (contextId) {
|
|
315
|
+
context = this.contexts.get(contextId);
|
|
316
|
+
if (!context) {
|
|
317
|
+
return new Response(
|
|
318
|
+
JSON.stringify({ error: `Context ${contextId} not found` }),
|
|
319
|
+
{
|
|
320
|
+
status: 404,
|
|
321
|
+
headers: { "Content-Type": "application/json" },
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
// Use or create default context for the language
|
|
327
|
+
const lang = language || "python";
|
|
328
|
+
context = await this.getOrCreateDefaultContext(lang);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!context) {
|
|
332
|
+
return new Response(JSON.stringify({ error: "No context available" }), {
|
|
333
|
+
status: 400,
|
|
334
|
+
headers: { "Content-Type": "application/json" },
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Update last used
|
|
339
|
+
context.lastUsed = new Date();
|
|
340
|
+
|
|
341
|
+
// Execute with streaming
|
|
342
|
+
return this.streamExecution(context.connection, code);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async streamExecution(
|
|
346
|
+
connection: Kernel.IKernelConnection,
|
|
347
|
+
code: string
|
|
348
|
+
): Promise<Response> {
|
|
349
|
+
const stream = new ReadableStream({
|
|
350
|
+
async start(controller) {
|
|
351
|
+
const future = connection.requestExecute({
|
|
352
|
+
code,
|
|
353
|
+
stop_on_error: false,
|
|
354
|
+
store_history: true,
|
|
355
|
+
silent: false,
|
|
356
|
+
allow_stdin: false,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Handle different message types
|
|
360
|
+
future.onIOPub = (msg: IIOPubMessage) => {
|
|
361
|
+
const result = processJupyterMessage(msg);
|
|
362
|
+
if (result) {
|
|
363
|
+
controller.enqueue(
|
|
364
|
+
new TextEncoder().encode(`${JSON.stringify(result)}\n`)
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
future.onReply = (msg: any) => {
|
|
370
|
+
if (msg.content.status === "ok") {
|
|
371
|
+
controller.enqueue(
|
|
372
|
+
new TextEncoder().encode(
|
|
373
|
+
`${JSON.stringify({
|
|
374
|
+
type: "execution_complete",
|
|
375
|
+
execution_count: msg.content.execution_count,
|
|
376
|
+
})}\n`
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
} else if (msg.content.status === "error") {
|
|
380
|
+
controller.enqueue(
|
|
381
|
+
new TextEncoder().encode(
|
|
382
|
+
`${JSON.stringify({
|
|
383
|
+
type: "error",
|
|
384
|
+
ename: msg.content.ename,
|
|
385
|
+
evalue: msg.content.evalue,
|
|
386
|
+
traceback: msg.content.traceback,
|
|
387
|
+
})}\n`
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
controller.close();
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
future.onStdin = (msg: any) => {
|
|
395
|
+
// We don't support stdin for now
|
|
396
|
+
console.warn("[JupyterServer] Stdin requested but not supported");
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return new Response(stream, {
|
|
402
|
+
headers: {
|
|
403
|
+
"Content-Type": "text/event-stream",
|
|
404
|
+
"Cache-Control": "no-cache",
|
|
405
|
+
Connection: "keep-alive",
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private getKernelName(language: string): string {
|
|
411
|
+
const kernelMap: Record<string, string> = {
|
|
412
|
+
python: "python3",
|
|
413
|
+
javascript: "javascript",
|
|
414
|
+
typescript: "javascript",
|
|
415
|
+
js: "javascript",
|
|
416
|
+
ts: "javascript",
|
|
417
|
+
};
|
|
418
|
+
return kernelMap[language.toLowerCase()] || "python3";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private async changeWorkingDirectory(context: JupyterContext, cwd: string) {
|
|
422
|
+
const code =
|
|
423
|
+
context.language === "python"
|
|
424
|
+
? `import os; os.chdir('${cwd}')`
|
|
425
|
+
: `process.chdir('${cwd}')`;
|
|
426
|
+
|
|
427
|
+
const future = context.connection.requestExecute({
|
|
428
|
+
code,
|
|
429
|
+
silent: true,
|
|
430
|
+
store_history: false,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return future.done;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private async setEnvironmentVariables(
|
|
437
|
+
context: JupyterContext,
|
|
438
|
+
envVars: Record<string, string>
|
|
439
|
+
) {
|
|
440
|
+
const commands: string[] = [];
|
|
441
|
+
|
|
442
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
443
|
+
if (context.language === "python") {
|
|
444
|
+
commands.push(`import os; os.environ['${key}'] = '${value}'`);
|
|
445
|
+
} else if (
|
|
446
|
+
context.language === "javascript" ||
|
|
447
|
+
context.language === "typescript"
|
|
448
|
+
) {
|
|
449
|
+
commands.push(`process.env['${key}'] = '${value}'`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (commands.length > 0) {
|
|
454
|
+
const code = commands.join("\n");
|
|
455
|
+
const future = context.connection.requestExecute({
|
|
456
|
+
code,
|
|
457
|
+
silent: true,
|
|
458
|
+
store_history: false,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
return future.done;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async listContexts(): Promise<
|
|
466
|
+
Array<{
|
|
467
|
+
id: string;
|
|
468
|
+
language: string;
|
|
469
|
+
cwd: string;
|
|
470
|
+
createdAt: Date;
|
|
471
|
+
lastUsed: Date;
|
|
472
|
+
}>
|
|
473
|
+
> {
|
|
474
|
+
return Array.from(this.contexts.values()).map((ctx) => ({
|
|
475
|
+
id: ctx.id,
|
|
476
|
+
language: ctx.language,
|
|
477
|
+
cwd: ctx.cwd,
|
|
478
|
+
createdAt: ctx.createdAt,
|
|
479
|
+
lastUsed: ctx.lastUsed,
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async deleteContext(contextId: string): Promise<void> {
|
|
484
|
+
const context = this.contexts.get(contextId);
|
|
485
|
+
if (!context) {
|
|
486
|
+
throw new Error(`Context ${contextId} not found`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Remove from active contexts map
|
|
490
|
+
this.contexts.delete(contextId);
|
|
491
|
+
|
|
492
|
+
// Remove from default contexts if it was a default
|
|
493
|
+
for (const [lang, id] of this.defaultContexts.entries()) {
|
|
494
|
+
if (id === contextId) {
|
|
495
|
+
this.defaultContexts.delete(lang);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// If it's a pooled context, release it back to the pool
|
|
501
|
+
if (context.pooled) {
|
|
502
|
+
this.releasePooledContext(context);
|
|
503
|
+
} else {
|
|
504
|
+
// Only shutdown non-pooled contexts
|
|
505
|
+
await context.connection.shutdown();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async shutdown() {
|
|
510
|
+
// Shutdown all active contexts
|
|
511
|
+
for (const context of this.contexts.values()) {
|
|
512
|
+
try {
|
|
513
|
+
await context.connection.shutdown();
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.error("[JupyterServer] Error shutting down kernel:", error);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Shutdown all pooled contexts
|
|
520
|
+
for (const pool of this.contextPools.values()) {
|
|
521
|
+
for (const context of pool.available) {
|
|
522
|
+
try {
|
|
523
|
+
await context.connection.shutdown();
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.error(
|
|
526
|
+
"[JupyterServer] Error shutting down pooled kernel:",
|
|
527
|
+
error
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this.contexts.clear();
|
|
534
|
+
this.defaultContexts.clear();
|
|
535
|
+
this.contextPools.clear();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get pool statistics for monitoring
|
|
540
|
+
*/
|
|
541
|
+
async getPoolStats(): Promise<
|
|
542
|
+
Record<
|
|
543
|
+
string,
|
|
544
|
+
{
|
|
545
|
+
available: number;
|
|
546
|
+
inUse: number;
|
|
547
|
+
total: number;
|
|
548
|
+
minSize: number;
|
|
549
|
+
maxSize: number;
|
|
550
|
+
warming: boolean;
|
|
551
|
+
}
|
|
552
|
+
>
|
|
553
|
+
> {
|
|
554
|
+
const stats: Record<
|
|
555
|
+
string,
|
|
556
|
+
{
|
|
557
|
+
available: number;
|
|
558
|
+
inUse: number;
|
|
559
|
+
total: number;
|
|
560
|
+
minSize: number;
|
|
561
|
+
maxSize: number;
|
|
562
|
+
warming: boolean;
|
|
563
|
+
}
|
|
564
|
+
> = {};
|
|
565
|
+
|
|
566
|
+
for (const [language, pool] of this.contextPools.entries()) {
|
|
567
|
+
stats[language] = {
|
|
568
|
+
available: pool.available.length,
|
|
569
|
+
inUse: pool.inUse.size,
|
|
570
|
+
total: pool.available.length + pool.inUse.size,
|
|
571
|
+
minSize: pool.minSize,
|
|
572
|
+
maxSize: pool.maxSize,
|
|
573
|
+
warming: pool.warming,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return stats;
|
|
578
|
+
}
|
|
579
|
+
}
|