@cloudflare/sandbox 0.3.7 → 0.4.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/.turbo/turbo-build.log +44 -0
- package/CHANGELOG.md +6 -14
- package/Dockerfile +82 -18
- package/README.md +89 -824
- package/dist/{chunk-JTKON2SH.js → chunk-BCJ7SF3Q.js} +9 -5
- package/dist/chunk-BCJ7SF3Q.js.map +1 -0
- package/dist/chunk-BFVUNTP4.js +104 -0
- package/dist/chunk-BFVUNTP4.js.map +1 -0
- package/dist/{chunk-NNGBXDMY.js → chunk-EKSWCBCA.js} +3 -6
- package/dist/chunk-EKSWCBCA.js.map +1 -0
- package/dist/chunk-HGF554LH.js +2236 -0
- package/dist/chunk-HGF554LH.js.map +1 -0
- package/dist/{chunk-6UAWTJ5S.js → chunk-Z532A7QC.js} +13 -20
- package/dist/{chunk-6UAWTJ5S.js.map → chunk-Z532A7QC.js.map} +1 -1
- package/dist/file-stream.d.ts +16 -38
- package/dist/file-stream.js +1 -2
- package/dist/index.d.ts +6 -5
- package/dist/index.js +35 -39
- package/dist/index.js.map +1 -1
- package/dist/interpreter.d.ts +3 -3
- package/dist/interpreter.js +2 -2
- package/dist/request-handler.d.ts +4 -3
- package/dist/request-handler.js +4 -7
- package/dist/sandbox-D9K2ypln.d.ts +583 -0
- package/dist/sandbox.d.ts +3 -3
- package/dist/sandbox.js +4 -7
- package/dist/security.d.ts +4 -3
- package/dist/security.js +3 -3
- package/dist/sse-parser.js +1 -1
- package/package.json +11 -5
- package/src/clients/base-client.ts +280 -0
- package/src/clients/command-client.ts +115 -0
- package/src/clients/file-client.ts +269 -0
- package/src/clients/git-client.ts +92 -0
- package/src/clients/index.ts +63 -0
- package/src/{interpreter-client.ts → clients/interpreter-client.ts} +148 -171
- package/src/clients/port-client.ts +105 -0
- package/src/clients/process-client.ts +177 -0
- package/src/clients/sandbox-client.ts +41 -0
- package/src/clients/types.ts +84 -0
- package/src/clients/utility-client.ts +94 -0
- package/src/errors/adapter.ts +180 -0
- package/src/errors/classes.ts +469 -0
- package/src/errors/index.ts +105 -0
- package/src/file-stream.ts +119 -117
- package/src/index.ts +81 -69
- package/src/interpreter.ts +17 -8
- package/src/request-handler.ts +69 -43
- package/src/sandbox.ts +694 -533
- package/src/security.ts +14 -23
- package/src/sse-parser.ts +4 -8
- package/startup.sh +3 -0
- package/tests/base-client.test.ts +328 -0
- package/tests/command-client.test.ts +407 -0
- package/tests/file-client.test.ts +643 -0
- package/tests/file-stream.test.ts +306 -0
- package/tests/git-client.test.ts +328 -0
- package/tests/port-client.test.ts +301 -0
- package/tests/process-client.test.ts +658 -0
- package/tests/sandbox.test.ts +465 -0
- package/tests/sse-parser.test.ts +290 -0
- package/tests/utility-client.test.ts +266 -0
- package/tests/wrangler.jsonc +35 -0
- package/tsconfig.json +9 -1
- package/vitest.config.ts +31 -0
- package/container_src/bun.lock +0 -76
- package/container_src/circuit-breaker.ts +0 -121
- package/container_src/control-process.ts +0 -784
- package/container_src/handler/exec.ts +0 -185
- package/container_src/handler/file.ts +0 -457
- package/container_src/handler/git.ts +0 -130
- package/container_src/handler/ports.ts +0 -314
- package/container_src/handler/process.ts +0 -568
- package/container_src/handler/session.ts +0 -92
- package/container_src/index.ts +0 -601
- package/container_src/interpreter-service.ts +0 -276
- package/container_src/isolation.ts +0 -1213
- package/container_src/mime-processor.ts +0 -255
- package/container_src/package.json +0 -18
- package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
- package/container_src/runtime/executors/python/ipython_executor.py +0 -338
- package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
- package/container_src/runtime/process-pool.ts +0 -464
- package/container_src/shell-escape.ts +0 -42
- package/container_src/startup.sh +0 -11
- package/container_src/types.ts +0 -131
- package/dist/chunk-32UDXUPC.js +0 -671
- package/dist/chunk-32UDXUPC.js.map +0 -1
- package/dist/chunk-5DILEXGY.js +0 -85
- package/dist/chunk-5DILEXGY.js.map +0 -1
- package/dist/chunk-D3U63BZP.js +0 -240
- package/dist/chunk-D3U63BZP.js.map +0 -1
- package/dist/chunk-FXYPFGOZ.js +0 -129
- package/dist/chunk-FXYPFGOZ.js.map +0 -1
- package/dist/chunk-JTKON2SH.js.map +0 -1
- package/dist/chunk-NNGBXDMY.js.map +0 -1
- package/dist/chunk-SQLJNZ3K.js +0 -674
- package/dist/chunk-SQLJNZ3K.js.map +0 -1
- package/dist/chunk-W7TVRPBG.js +0 -108
- package/dist/chunk-W7TVRPBG.js.map +0 -1
- package/dist/client-B3RUab0s.d.ts +0 -225
- package/dist/client.d.ts +0 -4
- package/dist/client.js +0 -7
- package/dist/client.js.map +0 -1
- package/dist/errors.d.ts +0 -95
- package/dist/errors.js +0 -27
- package/dist/errors.js.map +0 -1
- package/dist/interpreter-client.d.ts +0 -4
- package/dist/interpreter-client.js +0 -9
- package/dist/interpreter-client.js.map +0 -1
- package/dist/interpreter-types.d.ts +0 -259
- package/dist/interpreter-types.js +0 -9
- package/dist/interpreter-types.js.map +0 -1
- package/dist/types.d.ts +0 -453
- package/dist/types.js +0 -45
- package/dist/types.js.map +0 -1
- package/src/client.ts +0 -1048
- package/src/errors.ts +0 -219
- package/src/interpreter-types.ts +0 -390
- package/src/types.ts +0 -571
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
export type InterpreterLanguage = "python" | "javascript" | "typescript";
|
|
5
|
-
|
|
6
|
-
export interface InterpreterProcess {
|
|
7
|
-
id: string;
|
|
8
|
-
language: InterpreterLanguage;
|
|
9
|
-
process: ChildProcess;
|
|
10
|
-
sessionId?: string;
|
|
11
|
-
lastUsed: Date;
|
|
12
|
-
isAvailable: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ExecutionResult {
|
|
16
|
-
stdout: string;
|
|
17
|
-
stderr: string;
|
|
18
|
-
success: boolean;
|
|
19
|
-
executionId: string;
|
|
20
|
-
outputs?: RichOutput[];
|
|
21
|
-
error?: {
|
|
22
|
-
type: string;
|
|
23
|
-
message: string;
|
|
24
|
-
traceback?: string;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface RichOutput {
|
|
29
|
-
type: "text" | "image" | "jpeg" | "svg" | "html" | "json" | "latex" | "markdown" | "javascript" | "error";
|
|
30
|
-
data: string;
|
|
31
|
-
metadata?: Record<string, unknown>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface PoolConfig {
|
|
35
|
-
maxProcesses: number;
|
|
36
|
-
idleTimeout: number; // milliseconds
|
|
37
|
-
minSize: number;
|
|
38
|
-
preWarmScript?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ExecutorPoolConfig extends PoolConfig {
|
|
42
|
-
executor: InterpreterLanguage;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const DEFAULT_EXECUTOR_CONFIGS: Record<InterpreterLanguage, ExecutorPoolConfig> = {
|
|
46
|
-
python: {
|
|
47
|
-
executor: "python",
|
|
48
|
-
minSize: 3,
|
|
49
|
-
maxProcesses: 15,
|
|
50
|
-
idleTimeout: 5 * 60 * 1000, // 5 minutes
|
|
51
|
-
preWarmScript: `
|
|
52
|
-
import matplotlib.pyplot as plt
|
|
53
|
-
import pandas as pd
|
|
54
|
-
import numpy as np
|
|
55
|
-
import json
|
|
56
|
-
print(json.dumps({"status": "pre-warmed"}))
|
|
57
|
-
`
|
|
58
|
-
},
|
|
59
|
-
javascript: {
|
|
60
|
-
executor: "javascript",
|
|
61
|
-
minSize: 3,
|
|
62
|
-
maxProcesses: 10,
|
|
63
|
-
idleTimeout: 5 * 60 * 1000,
|
|
64
|
-
preWarmScript: `
|
|
65
|
-
const fs = require('fs');
|
|
66
|
-
const path = require('path');
|
|
67
|
-
const util = require('util');
|
|
68
|
-
const crypto = require('crypto');
|
|
69
|
-
for(let i = 0; i < 1000; i++) {
|
|
70
|
-
JSON.stringify({x: i, data: Math.random()});
|
|
71
|
-
}
|
|
72
|
-
console.log(JSON.stringify({"status": "pre-warmed"}));
|
|
73
|
-
`
|
|
74
|
-
},
|
|
75
|
-
typescript: {
|
|
76
|
-
executor: "typescript",
|
|
77
|
-
minSize: 3,
|
|
78
|
-
maxProcesses: 10,
|
|
79
|
-
idleTimeout: 5 * 60 * 1000,
|
|
80
|
-
preWarmScript: `
|
|
81
|
-
const { transformSync } = require('esbuild');
|
|
82
|
-
const warmupCode = 'interface Test { x: number; } const test: Test = { x: 42 }; test.x';
|
|
83
|
-
transformSync(warmupCode, { loader: 'ts', target: 'es2020', format: 'cjs' });
|
|
84
|
-
console.log(JSON.stringify({"status": "pre-warmed"}));
|
|
85
|
-
`
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
export class ProcessPoolManager {
|
|
90
|
-
private pools: Map<InterpreterLanguage, InterpreterProcess[]> = new Map();
|
|
91
|
-
private poolConfigs: Map<InterpreterLanguage, ExecutorPoolConfig> = new Map();
|
|
92
|
-
private cleanupInterval?: NodeJS.Timeout;
|
|
93
|
-
|
|
94
|
-
constructor(customConfigs: Partial<Record<InterpreterLanguage, Partial<ExecutorPoolConfig>>> = {}) {
|
|
95
|
-
const executorEntries = Object.entries(DEFAULT_EXECUTOR_CONFIGS) as [InterpreterLanguage, ExecutorPoolConfig][];
|
|
96
|
-
|
|
97
|
-
for (const [executor, defaultConfig] of executorEntries) {
|
|
98
|
-
const userConfig = customConfigs[executor] || {};
|
|
99
|
-
const envMinSize = process.env[`${executor.toUpperCase()}_POOL_MIN_SIZE`];
|
|
100
|
-
const envMaxSize = process.env[`${executor.toUpperCase()}_POOL_MAX_SIZE`];
|
|
101
|
-
|
|
102
|
-
const config: ExecutorPoolConfig = {
|
|
103
|
-
...defaultConfig,
|
|
104
|
-
...userConfig,
|
|
105
|
-
// Environment variables override user config override defaults
|
|
106
|
-
minSize: envMinSize ? parseInt(envMinSize) : (userConfig.minSize || defaultConfig.minSize),
|
|
107
|
-
maxProcesses: envMaxSize ? parseInt(envMaxSize) : (userConfig.maxProcesses || defaultConfig.maxProcesses)
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
this.poolConfigs.set(executor, config);
|
|
111
|
-
this.pools.set(executor, []);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const pythonConfig = this.poolConfigs.get("python");
|
|
115
|
-
if (pythonConfig) {
|
|
116
|
-
this.cleanupInterval = setInterval(() => {
|
|
117
|
-
this.cleanupIdleProcesses();
|
|
118
|
-
}, pythonConfig.idleTimeout / 2);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Start pre-warming in background - don't block constructor
|
|
122
|
-
this.startPreWarming().catch((error) => {
|
|
123
|
-
console.error('[ProcessPool] Pre-warming failed:', error);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async execute(
|
|
128
|
-
language: InterpreterLanguage,
|
|
129
|
-
code: string,
|
|
130
|
-
sessionId?: string,
|
|
131
|
-
timeout = 30000
|
|
132
|
-
): Promise<ExecutionResult> {
|
|
133
|
-
const totalStartTime = Date.now();
|
|
134
|
-
const process = await this.getProcess(language, sessionId);
|
|
135
|
-
const processAcquireTime = Date.now() - totalStartTime;
|
|
136
|
-
|
|
137
|
-
const executionId = randomUUID();
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const execStartTime = Date.now();
|
|
141
|
-
const result = await this.executeCode(process, code, executionId, timeout);
|
|
142
|
-
const execTime = Date.now() - execStartTime;
|
|
143
|
-
const totalTime = Date.now() - totalStartTime;
|
|
144
|
-
|
|
145
|
-
console.log(`[ProcessPool] Execution complete - Process acquire: ${processAcquireTime}ms, Code exec: ${execTime}ms, Total: ${totalTime}ms`);
|
|
146
|
-
return result;
|
|
147
|
-
} finally {
|
|
148
|
-
this.releaseProcess(process, sessionId);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
private async getProcess(
|
|
153
|
-
language: InterpreterLanguage,
|
|
154
|
-
sessionId?: string
|
|
155
|
-
): Promise<InterpreterProcess> {
|
|
156
|
-
const pool = this.pools.get(language)!;
|
|
157
|
-
|
|
158
|
-
if (sessionId) {
|
|
159
|
-
const existingProcess = pool.find(
|
|
160
|
-
(p) => p.sessionId === sessionId && p.isAvailable
|
|
161
|
-
);
|
|
162
|
-
if (existingProcess) {
|
|
163
|
-
existingProcess.isAvailable = false;
|
|
164
|
-
existingProcess.lastUsed = new Date();
|
|
165
|
-
return existingProcess;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const availableProcess = pool.find((p) => p.isAvailable && !p.sessionId);
|
|
170
|
-
if (availableProcess) {
|
|
171
|
-
availableProcess.isAvailable = false;
|
|
172
|
-
availableProcess.sessionId = sessionId;
|
|
173
|
-
availableProcess.lastUsed = new Date();
|
|
174
|
-
return availableProcess;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const config = this.poolConfigs.get(language)!;
|
|
178
|
-
if (pool.length < config.maxProcesses) {
|
|
179
|
-
const newProcess = await this.createProcess(language, sessionId);
|
|
180
|
-
pool.push(newProcess);
|
|
181
|
-
return newProcess;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return new Promise((resolve) => {
|
|
185
|
-
const checkForAvailable = () => {
|
|
186
|
-
const available = pool.find((p) => p.isAvailable);
|
|
187
|
-
if (available) {
|
|
188
|
-
available.isAvailable = false;
|
|
189
|
-
available.sessionId = sessionId;
|
|
190
|
-
available.lastUsed = new Date();
|
|
191
|
-
resolve(available);
|
|
192
|
-
} else {
|
|
193
|
-
setTimeout(checkForAvailable, 100);
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
checkForAvailable();
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private async createProcess(
|
|
201
|
-
language: InterpreterLanguage,
|
|
202
|
-
sessionId?: string
|
|
203
|
-
): Promise<InterpreterProcess> {
|
|
204
|
-
const startTime = Date.now();
|
|
205
|
-
const id = randomUUID();
|
|
206
|
-
let command: string;
|
|
207
|
-
let args: string[];
|
|
208
|
-
|
|
209
|
-
switch (language) {
|
|
210
|
-
case "python":
|
|
211
|
-
command = "python3";
|
|
212
|
-
args = ["-u", "/container-server/runtime/executors/python/ipython_executor.py"];
|
|
213
|
-
break;
|
|
214
|
-
case "javascript":
|
|
215
|
-
command = "node";
|
|
216
|
-
args = ["/container-server/runtime/executors/javascript/node_executor.js"];
|
|
217
|
-
break;
|
|
218
|
-
case "typescript":
|
|
219
|
-
command = "node";
|
|
220
|
-
args = ["/container-server/runtime/executors/typescript/ts_executor.js"];
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
console.log(`[ProcessPool] Spawning ${language} process: ${command} ${args.join(' ')}`);
|
|
225
|
-
|
|
226
|
-
const childProcess = spawn(command, args, {
|
|
227
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
228
|
-
env: {
|
|
229
|
-
...process.env,
|
|
230
|
-
PYTHONUNBUFFERED: "1",
|
|
231
|
-
NODE_NO_WARNINGS: "1",
|
|
232
|
-
},
|
|
233
|
-
cwd: "/workspace",
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const interpreterProcess: InterpreterProcess = {
|
|
237
|
-
id,
|
|
238
|
-
language,
|
|
239
|
-
process: childProcess,
|
|
240
|
-
sessionId,
|
|
241
|
-
lastUsed: new Date(),
|
|
242
|
-
isAvailable: false,
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
return new Promise((resolve, reject) => {
|
|
246
|
-
let readyBuffer = "";
|
|
247
|
-
let errorBuffer = "";
|
|
248
|
-
|
|
249
|
-
const timeout = setTimeout(() => {
|
|
250
|
-
childProcess.kill();
|
|
251
|
-
console.error(`[ProcessPool] ${language} executor timeout. stdout: "${readyBuffer}", stderr: "${errorBuffer}"`);
|
|
252
|
-
reject(new Error(`${language} executor failed to start`));
|
|
253
|
-
}, 5000);
|
|
254
|
-
|
|
255
|
-
const readyHandler = (data: Buffer) => {
|
|
256
|
-
readyBuffer += data.toString();
|
|
257
|
-
console.log(`[ProcessPool] ${language} stdout:`, data.toString());
|
|
258
|
-
|
|
259
|
-
if (readyBuffer.includes('"ready"')) {
|
|
260
|
-
clearTimeout(timeout);
|
|
261
|
-
childProcess.stdout?.removeListener("data", readyHandler);
|
|
262
|
-
childProcess.stderr?.removeListener("data", errorHandler);
|
|
263
|
-
const readyTime = Date.now() - startTime;
|
|
264
|
-
console.log(`[ProcessPool] ${language} process ${id} ready in ${readyTime}ms`);
|
|
265
|
-
resolve(interpreterProcess);
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const errorHandler = (data: Buffer) => {
|
|
270
|
-
errorBuffer += data.toString();
|
|
271
|
-
console.error(`[ProcessPool] ${language} stderr:`, data.toString());
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
childProcess.stdout?.on("data", readyHandler);
|
|
275
|
-
childProcess.stderr?.on("data", errorHandler);
|
|
276
|
-
|
|
277
|
-
childProcess.once("error", (err) => {
|
|
278
|
-
clearTimeout(timeout);
|
|
279
|
-
console.error(`[ProcessPool] ${language} spawn error:`, err);
|
|
280
|
-
reject(err);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
childProcess.once("exit", (code) => {
|
|
284
|
-
if (code !== 0) {
|
|
285
|
-
clearTimeout(timeout);
|
|
286
|
-
console.error(`[ProcessPool] ${language} exited with code ${code}`);
|
|
287
|
-
reject(new Error(`${language} executor exited with code ${code}`));
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private async executeCode(
|
|
294
|
-
process: InterpreterProcess,
|
|
295
|
-
code: string,
|
|
296
|
-
executionId: string,
|
|
297
|
-
timeout: number
|
|
298
|
-
): Promise<ExecutionResult> {
|
|
299
|
-
const request = JSON.stringify({ code, executionId });
|
|
300
|
-
|
|
301
|
-
return new Promise((resolve, reject) => {
|
|
302
|
-
const timer = setTimeout(() => {
|
|
303
|
-
reject(new Error("Execution timeout"));
|
|
304
|
-
}, timeout);
|
|
305
|
-
|
|
306
|
-
let responseBuffer = "";
|
|
307
|
-
|
|
308
|
-
const responseHandler = (data: Buffer) => {
|
|
309
|
-
responseBuffer += data.toString();
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
const response = JSON.parse(responseBuffer);
|
|
313
|
-
clearTimeout(timer);
|
|
314
|
-
process.process.stdout?.removeListener("data", responseHandler);
|
|
315
|
-
|
|
316
|
-
resolve({
|
|
317
|
-
stdout: response.stdout || "",
|
|
318
|
-
stderr: response.stderr || "",
|
|
319
|
-
success: response.success !== false,
|
|
320
|
-
executionId,
|
|
321
|
-
outputs: response.outputs || [],
|
|
322
|
-
error: response.error || null,
|
|
323
|
-
});
|
|
324
|
-
} catch (e) {
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
process.process.stdout?.on("data", responseHandler);
|
|
329
|
-
process.process.stdin?.write(`${request}\n`);
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
private releaseProcess(
|
|
334
|
-
process: InterpreterProcess,
|
|
335
|
-
sessionId?: string
|
|
336
|
-
): void {
|
|
337
|
-
if (!sessionId) {
|
|
338
|
-
process.sessionId = undefined;
|
|
339
|
-
process.isAvailable = true;
|
|
340
|
-
} else {
|
|
341
|
-
process.isAvailable = true;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
private async startPreWarming(): Promise<void> {
|
|
346
|
-
console.log('[ProcessPool] Starting unified pre-warming for all executors...');
|
|
347
|
-
const startTime = Date.now();
|
|
348
|
-
|
|
349
|
-
const warmupPromises = Array.from(this.poolConfigs.entries()).map(
|
|
350
|
-
async ([executor, config]) => {
|
|
351
|
-
if (config.minSize > 0) {
|
|
352
|
-
await this.preWarmExecutor(executor, config);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
await Promise.all(warmupPromises);
|
|
359
|
-
const totalTime = Date.now() - startTime;
|
|
360
|
-
console.log(`[ProcessPool] Pre-warming complete for all executors in ${totalTime}ms`);
|
|
361
|
-
} catch (error) {
|
|
362
|
-
console.error('[ProcessPool] Pre-warming failed:', error);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private async preWarmExecutor(executor: InterpreterLanguage, config: ExecutorPoolConfig): Promise<void> {
|
|
367
|
-
const startTime = Date.now();
|
|
368
|
-
console.log(`[ProcessPool] Pre-warming ${config.minSize} ${executor} processes...`);
|
|
369
|
-
|
|
370
|
-
const pool = this.pools.get(executor);
|
|
371
|
-
if (!pool) {
|
|
372
|
-
console.error(`[ProcessPool] No pool found for executor: ${executor}`);
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
for (let i = 0; i < config.minSize; i++) {
|
|
377
|
-
try {
|
|
378
|
-
const sessionId = `pre-warm-${executor}-${i}-${Date.now()}`;
|
|
379
|
-
const process = await this.createProcess(executor, sessionId);
|
|
380
|
-
|
|
381
|
-
if (config.preWarmScript) {
|
|
382
|
-
await this.executePreWarmScript(process, config.preWarmScript, executor);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
process.isAvailable = true;
|
|
386
|
-
process.sessionId = undefined;
|
|
387
|
-
pool.push(process);
|
|
388
|
-
} catch (error) {
|
|
389
|
-
console.error(`[ProcessPool] Failed to pre-warm ${executor} process ${i}:`, error);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const warmupTime = Date.now() - startTime;
|
|
394
|
-
const actualCount = pool.filter(p => p.isAvailable).length;
|
|
395
|
-
console.log(`[ProcessPool] Pre-warmed ${actualCount}/${config.minSize} ${executor} processes in ${warmupTime}ms`);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
private async executePreWarmScript(
|
|
399
|
-
process: InterpreterProcess,
|
|
400
|
-
script: string,
|
|
401
|
-
executor: InterpreterLanguage
|
|
402
|
-
): Promise<void> {
|
|
403
|
-
try {
|
|
404
|
-
const executionId = `pre-warm-${Date.now()}`;
|
|
405
|
-
const result = await this.executeCode(process, script, executionId, 10000);
|
|
406
|
-
|
|
407
|
-
if (result.success) {
|
|
408
|
-
console.log(`[ProcessPool] ${executor} pre-warm script executed successfully`);
|
|
409
|
-
} else {
|
|
410
|
-
console.warn(`[ProcessPool] ${executor} pre-warm script failed:`, result.stderr);
|
|
411
|
-
}
|
|
412
|
-
} catch (error) {
|
|
413
|
-
console.warn(`[ProcessPool] ${executor} pre-warm script error:`, error);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
private cleanupIdleProcesses(): void {
|
|
418
|
-
const now = new Date();
|
|
419
|
-
|
|
420
|
-
const executors = Array.from(this.pools.keys());
|
|
421
|
-
for (const executor of executors) {
|
|
422
|
-
const pool = this.pools.get(executor);
|
|
423
|
-
const config = this.poolConfigs.get(executor);
|
|
424
|
-
|
|
425
|
-
if (!pool || !config) {
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
for (let i = pool.length - 1; i >= 0; i--) {
|
|
430
|
-
const process = pool[i];
|
|
431
|
-
const idleTime = now.getTime() - process.lastUsed.getTime();
|
|
432
|
-
|
|
433
|
-
// Only clean up excess processes beyond minimum pool size
|
|
434
|
-
if (process.isAvailable &&
|
|
435
|
-
idleTime > config.idleTimeout &&
|
|
436
|
-
pool.filter(p => p.isAvailable).length > config.minSize) {
|
|
437
|
-
process.process.kill();
|
|
438
|
-
pool.splice(i, 1);
|
|
439
|
-
console.log(`[ProcessPool] Cleaned up idle ${executor} process (${pool.length} remaining)`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async shutdown(): Promise<void> {
|
|
446
|
-
if (this.cleanupInterval) {
|
|
447
|
-
clearInterval(this.cleanupInterval);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const executors = Array.from(this.pools.keys());
|
|
451
|
-
for (const executor of executors) {
|
|
452
|
-
const pool = this.pools.get(executor);
|
|
453
|
-
if (pool) {
|
|
454
|
-
for (const process of pool) {
|
|
455
|
-
process.process.kill();
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
this.pools.clear();
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
export const processPool = new ProcessPoolManager();
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Secure shell command utilities to prevent injection attacks
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Escapes a string for safe use in shell commands.
|
|
7
|
-
* This follows POSIX shell escaping rules to prevent command injection.
|
|
8
|
-
*
|
|
9
|
-
* @param str - The string to escape
|
|
10
|
-
* @returns The escaped string safe for shell use
|
|
11
|
-
*/
|
|
12
|
-
export function escapeShellArg(str: string): string {
|
|
13
|
-
// If string is empty, return empty quotes
|
|
14
|
-
if (str === '') {
|
|
15
|
-
return "''";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Check if string contains any characters that need escaping
|
|
19
|
-
// Safe characters: alphanumeric, dash, underscore, dot, slash
|
|
20
|
-
if (/^[a-zA-Z0-9._\-/]+$/.test(str)) {
|
|
21
|
-
return str;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// For strings with special characters, use single quotes and escape single quotes
|
|
25
|
-
// Single quotes preserve all characters literally except the single quote itself
|
|
26
|
-
// To include a single quote, we end the quoted string, add an escaped quote, and start a new quoted string
|
|
27
|
-
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Escapes a file path for safe use in shell commands.
|
|
32
|
-
*
|
|
33
|
-
* @param path - The file path to escape
|
|
34
|
-
* @returns The escaped path safe for shell use
|
|
35
|
-
*/
|
|
36
|
-
export function escapeShellPath(path: string): string {
|
|
37
|
-
// Normalize path to prevent issues with multiple slashes
|
|
38
|
-
const normalizedPath = path.replace(/\/+/g, '/');
|
|
39
|
-
|
|
40
|
-
// Apply standard shell escaping
|
|
41
|
-
return escapeShellArg(normalizedPath);
|
|
42
|
-
}
|
package/container_src/startup.sh
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
echo "[Startup] Starting interpreter server..."
|
|
4
|
-
echo "[Startup] Process pool system initialized"
|
|
5
|
-
|
|
6
|
-
echo "[Startup] Environment configured:"
|
|
7
|
-
echo " - Working directory: $(pwd)"
|
|
8
|
-
|
|
9
|
-
# Start Bun server - the only process we need now
|
|
10
|
-
echo "[Startup] Starting Bun server..."
|
|
11
|
-
exec bun index.ts
|
package/container_src/types.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import type { ChildProcess } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
// Process management types
|
|
4
|
-
export type ProcessStatus =
|
|
5
|
-
| 'starting'
|
|
6
|
-
| 'running'
|
|
7
|
-
| 'completed'
|
|
8
|
-
| 'failed'
|
|
9
|
-
| 'killed'
|
|
10
|
-
| 'error';
|
|
11
|
-
|
|
12
|
-
export interface ProcessRecord {
|
|
13
|
-
id: string;
|
|
14
|
-
pid?: number;
|
|
15
|
-
command: string;
|
|
16
|
-
status: ProcessStatus;
|
|
17
|
-
startTime: Date;
|
|
18
|
-
endTime?: Date;
|
|
19
|
-
exitCode?: number;
|
|
20
|
-
stdoutFile?: string; // Path to temp file containing stdout
|
|
21
|
-
stderrFile?: string; // Path to temp file containing stderr
|
|
22
|
-
stdout: string;
|
|
23
|
-
stderr: string;
|
|
24
|
-
outputListeners: Set<(stream: 'stdout' | 'stderr', data: string) => void>;
|
|
25
|
-
statusListeners: Set<(status: ProcessStatus) => void>;
|
|
26
|
-
monitoringInterval?: NodeJS.Timeout; // For polling temp files when streaming
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface StartProcessRequest {
|
|
30
|
-
command: string;
|
|
31
|
-
sessionId: string;
|
|
32
|
-
options?: {
|
|
33
|
-
processId?: string;
|
|
34
|
-
timeout?: number;
|
|
35
|
-
env?: Record<string, string>;
|
|
36
|
-
cwd?: string;
|
|
37
|
-
encoding?: string;
|
|
38
|
-
autoCleanup?: boolean;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface ExecuteOptions {
|
|
43
|
-
background?: boolean;
|
|
44
|
-
cwd?: string | URL;
|
|
45
|
-
env?: Record<string, string>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ExecuteRequest extends ExecuteOptions {
|
|
49
|
-
command: string;
|
|
50
|
-
sessionId: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface GitCheckoutRequest {
|
|
54
|
-
repoUrl: string;
|
|
55
|
-
branch?: string;
|
|
56
|
-
targetDir?: string;
|
|
57
|
-
sessionId: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface MkdirRequest {
|
|
61
|
-
path: string;
|
|
62
|
-
recursive?: boolean;
|
|
63
|
-
sessionId: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface WriteFileRequest {
|
|
67
|
-
path: string;
|
|
68
|
-
content: string;
|
|
69
|
-
encoding?: string;
|
|
70
|
-
sessionId: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface ReadFileRequest {
|
|
74
|
-
path: string;
|
|
75
|
-
encoding?: string;
|
|
76
|
-
sessionId: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface DeleteFileRequest {
|
|
80
|
-
path: string;
|
|
81
|
-
sessionId: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface RenameFileRequest {
|
|
85
|
-
oldPath: string;
|
|
86
|
-
newPath: string;
|
|
87
|
-
sessionId: string;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface MoveFileRequest {
|
|
91
|
-
sourcePath: string;
|
|
92
|
-
destinationPath: string;
|
|
93
|
-
sessionId: string;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface ListFilesRequest {
|
|
97
|
-
path: string;
|
|
98
|
-
options?: {
|
|
99
|
-
recursive?: boolean;
|
|
100
|
-
includeHidden?: boolean;
|
|
101
|
-
};
|
|
102
|
-
sessionId: string;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export interface ExposePortRequest {
|
|
106
|
-
port: number;
|
|
107
|
-
name?: string;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface UnexposePortRequest {
|
|
111
|
-
port: number;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface SessionData {
|
|
115
|
-
id: string;
|
|
116
|
-
activeProcess: ChildProcess | null;
|
|
117
|
-
createdAt: Date;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Session management API types
|
|
121
|
-
export interface CreateSessionRequest {
|
|
122
|
-
id: string;
|
|
123
|
-
env?: Record<string, string>;
|
|
124
|
-
cwd?: string;
|
|
125
|
-
isolation?: boolean;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface SessionExecRequest {
|
|
129
|
-
id: string;
|
|
130
|
-
command: string;
|
|
131
|
-
}
|