@cloudflare/sandbox 0.0.0-d81d2a5 → 0.0.0-d951819
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 +129 -0
- package/Dockerfile +34 -27
- package/README.md +127 -12
- package/container_src/bun.lock +31 -77
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/control-process.ts +784 -0
- package/container_src/handler/exec.ts +99 -254
- package/container_src/handler/file.ts +253 -640
- package/container_src/handler/git.ts +28 -80
- package/container_src/handler/process.ts +443 -515
- package/container_src/handler/session.ts +92 -0
- package/container_src/index.ts +289 -219
- package/container_src/interpreter-service.ts +276 -0
- package/container_src/isolation.ts +1213 -0
- package/container_src/mime-processor.ts +1 -1
- package/container_src/package.json +4 -4
- package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
- package/container_src/runtime/executors/python/ipython_executor.py +338 -0
- package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
- package/container_src/runtime/process-pool.ts +464 -0
- package/container_src/shell-escape.ts +42 -0
- package/container_src/startup.sh +6 -47
- package/container_src/types.ts +35 -12
- package/package.json +2 -2
- package/src/client.ts +214 -187
- package/src/errors.ts +219 -0
- package/src/file-stream.ts +162 -0
- package/src/index.ts +66 -14
- package/src/interpreter-client.ts +352 -0
- package/src/interpreter-types.ts +102 -95
- package/src/interpreter.ts +8 -8
- package/src/sandbox.ts +315 -337
- package/src/types.ts +194 -24
- package/container_src/jupyter-server.ts +0 -336
- package/src/jupyter-client.ts +0 -266
package/src/types.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
// Core Types
|
|
2
2
|
|
|
3
3
|
export interface BaseExecOptions {
|
|
4
|
-
/**
|
|
5
|
-
* Session ID for grouping related commands
|
|
6
|
-
*/
|
|
7
|
-
sessionId?: string;
|
|
8
|
-
|
|
9
4
|
/**
|
|
10
5
|
* Maximum execution time in milliseconds
|
|
11
6
|
*/
|
|
@@ -90,11 +85,6 @@ export interface ExecResult {
|
|
|
90
85
|
* ISO timestamp when command started
|
|
91
86
|
*/
|
|
92
87
|
timestamp: string;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Session ID if provided
|
|
96
|
-
*/
|
|
97
|
-
sessionId?: string;
|
|
98
88
|
}
|
|
99
89
|
|
|
100
90
|
// Background Process Types
|
|
@@ -177,11 +167,6 @@ export interface Process {
|
|
|
177
167
|
*/
|
|
178
168
|
readonly exitCode?: number;
|
|
179
169
|
|
|
180
|
-
/**
|
|
181
|
-
* Session ID if provided
|
|
182
|
-
*/
|
|
183
|
-
readonly sessionId?: string;
|
|
184
|
-
|
|
185
170
|
/**
|
|
186
171
|
* Kill the process
|
|
187
172
|
*/
|
|
@@ -208,7 +193,6 @@ export interface ExecEvent {
|
|
|
208
193
|
exitCode?: number;
|
|
209
194
|
result?: ExecResult;
|
|
210
195
|
error?: string; // Changed to string for serialization
|
|
211
|
-
sessionId?: string;
|
|
212
196
|
}
|
|
213
197
|
|
|
214
198
|
export interface LogEvent {
|
|
@@ -216,7 +200,6 @@ export interface LogEvent {
|
|
|
216
200
|
timestamp: string;
|
|
217
201
|
data: string;
|
|
218
202
|
processId: string;
|
|
219
|
-
sessionId?: string;
|
|
220
203
|
exitCode?: number; // For 'exit' events
|
|
221
204
|
}
|
|
222
205
|
|
|
@@ -232,6 +215,54 @@ export interface StreamOptions extends BaseExecOptions {
|
|
|
232
215
|
signal?: AbortSignal;
|
|
233
216
|
}
|
|
234
217
|
|
|
218
|
+
// File Streaming Types
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* SSE events for file streaming
|
|
222
|
+
*/
|
|
223
|
+
export type FileStreamEvent =
|
|
224
|
+
| {
|
|
225
|
+
type: 'metadata';
|
|
226
|
+
mimeType: string;
|
|
227
|
+
size: number;
|
|
228
|
+
isBinary: boolean;
|
|
229
|
+
encoding: 'utf-8' | 'base64';
|
|
230
|
+
}
|
|
231
|
+
| {
|
|
232
|
+
type: 'chunk';
|
|
233
|
+
data: string; // base64 for binary, UTF-8 for text
|
|
234
|
+
}
|
|
235
|
+
| {
|
|
236
|
+
type: 'complete';
|
|
237
|
+
bytesRead: number;
|
|
238
|
+
}
|
|
239
|
+
| {
|
|
240
|
+
type: 'error';
|
|
241
|
+
error: string;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* File metadata from streaming
|
|
246
|
+
*/
|
|
247
|
+
export interface FileMetadata {
|
|
248
|
+
mimeType: string;
|
|
249
|
+
size: number;
|
|
250
|
+
isBinary: boolean;
|
|
251
|
+
encoding: 'utf-8' | 'base64';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* File stream chunk - either string (text) or Uint8Array (binary, auto-decoded)
|
|
256
|
+
*/
|
|
257
|
+
export type FileChunk = string | Uint8Array;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* AsyncIterable of file chunks with metadata
|
|
261
|
+
*/
|
|
262
|
+
export interface FileStream extends AsyncIterable<FileChunk> {
|
|
263
|
+
metadata?: FileMetadata;
|
|
264
|
+
}
|
|
265
|
+
|
|
235
266
|
// Error Types
|
|
236
267
|
|
|
237
268
|
export class SandboxError extends Error {
|
|
@@ -272,10 +303,8 @@ export interface ProcessRecord {
|
|
|
272
303
|
startTime: Date;
|
|
273
304
|
endTime?: Date;
|
|
274
305
|
exitCode?: number;
|
|
275
|
-
sessionId?: string;
|
|
276
306
|
|
|
277
307
|
// Internal fields
|
|
278
|
-
childProcess?: any; // Node.js ChildProcess
|
|
279
308
|
stdout: string; // Accumulated output (ephemeral)
|
|
280
309
|
stderr: string; // Accumulated output (ephemeral)
|
|
281
310
|
|
|
@@ -290,7 +319,6 @@ export interface StartProcessRequest {
|
|
|
290
319
|
command: string;
|
|
291
320
|
options?: {
|
|
292
321
|
processId?: string;
|
|
293
|
-
sessionId?: string;
|
|
294
322
|
timeout?: number;
|
|
295
323
|
env?: Record<string, string>;
|
|
296
324
|
cwd?: string;
|
|
@@ -304,9 +332,11 @@ export interface StartProcessResponse {
|
|
|
304
332
|
id: string;
|
|
305
333
|
pid?: number;
|
|
306
334
|
command: string;
|
|
307
|
-
|
|
335
|
+
status: ProcessStatus;
|
|
308
336
|
startTime: string;
|
|
309
|
-
|
|
337
|
+
endTime?: string | null;
|
|
338
|
+
exitCode?: number | null;
|
|
339
|
+
sessionId: string;
|
|
310
340
|
};
|
|
311
341
|
}
|
|
312
342
|
|
|
@@ -319,7 +349,6 @@ export interface ListProcessesResponse {
|
|
|
319
349
|
startTime: string;
|
|
320
350
|
endTime?: string;
|
|
321
351
|
exitCode?: number;
|
|
322
|
-
sessionId?: string;
|
|
323
352
|
}>;
|
|
324
353
|
}
|
|
325
354
|
|
|
@@ -332,7 +361,6 @@ export interface GetProcessResponse {
|
|
|
332
361
|
startTime: string;
|
|
333
362
|
endTime?: string;
|
|
334
363
|
exitCode?: number;
|
|
335
|
-
sessionId?: string;
|
|
336
364
|
} | null;
|
|
337
365
|
}
|
|
338
366
|
|
|
@@ -371,6 +399,26 @@ export interface ISandbox {
|
|
|
371
399
|
cleanupCompletedProcesses(): Promise<number>;
|
|
372
400
|
getProcessLogs(id: string): Promise<{ stdout: string; stderr: string }>;
|
|
373
401
|
|
|
402
|
+
// File operations
|
|
403
|
+
gitCheckout(repoUrl: string, options: { branch?: string; targetDir?: string }): Promise<GitCheckoutResponse>;
|
|
404
|
+
mkdir(path: string, options?: { recursive?: boolean }): Promise<MkdirResponse>;
|
|
405
|
+
writeFile(path: string, content: string, options?: { encoding?: string }): Promise<WriteFileResponse>;
|
|
406
|
+
deleteFile(path: string): Promise<DeleteFileResponse>;
|
|
407
|
+
renameFile(oldPath: string, newPath: string): Promise<RenameFileResponse>;
|
|
408
|
+
moveFile(sourcePath: string, destinationPath: string): Promise<MoveFileResponse>;
|
|
409
|
+
readFile(path: string, options?: { encoding?: string }): Promise<ReadFileResponse>;
|
|
410
|
+
readFileStream(path: string): Promise<ReadableStream<Uint8Array>>;
|
|
411
|
+
listFiles(path: string, options?: { recursive?: boolean; includeHidden?: boolean }): Promise<ListFilesResponse>;
|
|
412
|
+
|
|
413
|
+
// Port management
|
|
414
|
+
exposePort(port: number, options: { name?: string; hostname: string }): Promise<{ url: string; port: number; name?: string }>;
|
|
415
|
+
unexposePort(port: number): Promise<void>;
|
|
416
|
+
getExposedPorts(hostname: string): Promise<Array<{ url: string; port: number; name?: string; exposedAt: string }>>;
|
|
417
|
+
|
|
418
|
+
// Environment management
|
|
419
|
+
setEnvVars(envVars: Record<string, string>): Promise<void>;
|
|
420
|
+
setSandboxName(name: string): Promise<void>;
|
|
421
|
+
|
|
374
422
|
// Code Interpreter API
|
|
375
423
|
createCodeContext(options?: CreateContextOptions): Promise<CodeContext>;
|
|
376
424
|
runCode(code: string, options?: RunCodeOptions): Promise<ExecutionResult>;
|
|
@@ -379,6 +427,128 @@ export interface ISandbox {
|
|
|
379
427
|
deleteCodeContext(contextId: string): Promise<void>;
|
|
380
428
|
}
|
|
381
429
|
|
|
430
|
+
// Execution session returned by createSession()
|
|
431
|
+
// Sessions are full-featured sandbox objects with scoped execution context
|
|
432
|
+
// Inherits all ISandbox methods except createSession (sessions can't create sub-sessions),
|
|
433
|
+
// and setSandboxName (sessions inherit sandbox name).
|
|
434
|
+
export interface ExecutionSession extends Omit<ISandbox, 'createSession' | 'setSandboxName'> {
|
|
435
|
+
/**
|
|
436
|
+
* Session ID
|
|
437
|
+
*/
|
|
438
|
+
id: string;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// API Response Types
|
|
442
|
+
|
|
443
|
+
export interface ExecuteResponse {
|
|
444
|
+
success: boolean;
|
|
445
|
+
stdout: string;
|
|
446
|
+
stderr: string;
|
|
447
|
+
exitCode: number;
|
|
448
|
+
command: string;
|
|
449
|
+
timestamp: string;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export interface GitCheckoutResponse {
|
|
453
|
+
success: boolean;
|
|
454
|
+
stdout: string;
|
|
455
|
+
stderr: string;
|
|
456
|
+
exitCode: number;
|
|
457
|
+
repoUrl: string;
|
|
458
|
+
branch: string;
|
|
459
|
+
targetDir: string;
|
|
460
|
+
timestamp: string;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export interface MkdirResponse {
|
|
464
|
+
success: boolean;
|
|
465
|
+
stdout: string;
|
|
466
|
+
stderr: string;
|
|
467
|
+
exitCode: number;
|
|
468
|
+
path: string;
|
|
469
|
+
recursive: boolean;
|
|
470
|
+
timestamp: string;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export interface WriteFileResponse {
|
|
474
|
+
success: boolean;
|
|
475
|
+
exitCode: number;
|
|
476
|
+
path: string;
|
|
477
|
+
timestamp: string;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export interface ReadFileResponse {
|
|
481
|
+
success: boolean;
|
|
482
|
+
exitCode: number;
|
|
483
|
+
path: string;
|
|
484
|
+
content: string;
|
|
485
|
+
timestamp: string;
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Encoding used for content (utf-8 for text, base64 for binary)
|
|
489
|
+
*/
|
|
490
|
+
encoding?: 'utf-8' | 'base64';
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Whether the file is detected as binary
|
|
494
|
+
*/
|
|
495
|
+
isBinary?: boolean;
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* MIME type of the file (e.g., 'image/png', 'text/plain')
|
|
499
|
+
*/
|
|
500
|
+
mimeType?: string;
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* File size in bytes
|
|
504
|
+
*/
|
|
505
|
+
size?: number;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export interface DeleteFileResponse {
|
|
509
|
+
success: boolean;
|
|
510
|
+
exitCode: number;
|
|
511
|
+
path: string;
|
|
512
|
+
timestamp: string;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export interface RenameFileResponse {
|
|
516
|
+
success: boolean;
|
|
517
|
+
exitCode: number;
|
|
518
|
+
oldPath: string;
|
|
519
|
+
newPath: string;
|
|
520
|
+
timestamp: string;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export interface MoveFileResponse {
|
|
524
|
+
success: boolean;
|
|
525
|
+
exitCode: number;
|
|
526
|
+
sourcePath: string;
|
|
527
|
+
destinationPath: string;
|
|
528
|
+
timestamp: string;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export interface ListFilesResponse {
|
|
532
|
+
success: boolean;
|
|
533
|
+
exitCode: number;
|
|
534
|
+
path: string;
|
|
535
|
+
files: Array<{
|
|
536
|
+
name: string;
|
|
537
|
+
absolutePath: string;
|
|
538
|
+
relativePath: string;
|
|
539
|
+
type: 'file' | 'directory' | 'symlink' | 'other';
|
|
540
|
+
size: number;
|
|
541
|
+
modifiedAt: string;
|
|
542
|
+
mode: string;
|
|
543
|
+
permissions: {
|
|
544
|
+
readable: boolean;
|
|
545
|
+
writable: boolean;
|
|
546
|
+
executable: boolean;
|
|
547
|
+
};
|
|
548
|
+
}>;
|
|
549
|
+
timestamp: string;
|
|
550
|
+
}
|
|
551
|
+
|
|
382
552
|
// Type Guards
|
|
383
553
|
|
|
384
554
|
export function isExecResult(value: any): value is ExecResult {
|
|
@@ -1,336 +0,0 @@
|
|
|
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
|
-
|