RaBU1234 commited on
Commit
f12014d
·
verified ·
1 Parent(s): 1448de0

Update simple-sandbox-manager.js

Browse files
Files changed (1) hide show
  1. simple-sandbox-manager.js +24 -375
simple-sandbox-manager.js CHANGED
@@ -1,375 +1,24 @@
1
- const { spawn, exec } = require('child_process');
2
- const fs = require('fs').promises;
3
- const path = require('path');
4
- const { EventEmitter } = require('events');
5
- const FirecrackerManager = require('./firecracker-manager');
6
-
7
- class SimpleSandboxManager extends EventEmitter {
8
- constructor() {
9
- super();
10
- this.sandboxes = new Map();
11
- this.processes = new Map();
12
- this.basePath = '/tmp/sandboxes';
13
- this.portCounter = 3000;
14
- this.streamClients = new Map();
15
- this.commandData = new Map();
16
- this.firecrackerManager = new FirecrackerManager();
17
- this.useFirecracker = false;
18
- }
19
-
20
- async initialize() {
21
- await fs.mkdir(this.basePath, { recursive: true });
22
-
23
- // Initialize Firecracker
24
- await this.firecrackerManager.initialize();
25
- this.useFirecracker = this.firecrackerManager.firecrackerAvailable;
26
-
27
- console.log(`✅ Sandbox Manager initialized (Firecracker: ${this.useFirecracker ? 'enabled' : 'disabled'})`);
28
- }
29
-
30
- async createSandbox(sandboxId, options = {}) {
31
- const { timeout = 600000, ports = [] } = options;
32
- const sandboxPath = path.join(this.basePath, sandboxId);
33
- await fs.mkdir(sandboxPath, { recursive: true });
34
-
35
- const workspacePath = path.join(sandboxPath, 'workspace');
36
- await fs.mkdir(workspacePath, { recursive: true });
37
-
38
- const port = this.portCounter++;
39
-
40
- const sandbox = {
41
- sandboxId,
42
- path: sandboxPath,
43
- workspacePath,
44
- port,
45
- exposedPorts: ports,
46
- timeout,
47
- createdAt: Date.now(),
48
- lastActivity: Date.now(),
49
- status: 'ready',
50
- vm: null
51
- };
52
-
53
- // Create Firecracker VM if available
54
- if (this.useFirecracker) {
55
- try {
56
- const vm = await this.firecrackerManager.createVM(sandboxId);
57
- await this.firecrackerManager.startVM(sandboxId);
58
- sandbox.vm = vm;
59
- console.log(`🔥 Firecracker VM created for ${sandboxId}`);
60
- } catch (error) {
61
- console.warn(`⚠️ Firecracker VM creation failed, falling back: ${error.message}`);
62
- this.useFirecracker = false;
63
- }
64
- }
65
-
66
- this.sandboxes.set(sandboxId, sandbox);
67
- console.log(`✅ Sandbox created: ${sandboxId} (port: ${port})`);
68
-
69
- return sandbox;
70
- }
71
-
72
- addStreamClient(sandboxId, commandId, res) {
73
- const key = `${sandboxId}:${commandId}`;
74
- if (!this.streamClients.has(key)) {
75
- this.streamClients.set(key, []);
76
- }
77
- this.streamClients.get(key).push(res);
78
-
79
- res.on('close', () => {
80
- const clients = this.streamClients.get(key);
81
- if (clients) {
82
- const index = clients.indexOf(res);
83
- if (index > -1) clients.splice(index, 1);
84
- }
85
- });
86
- }
87
-
88
- sendToStreamClients(sandboxId, commandId, type, data) {
89
- const key = `${sandboxId}:${commandId}`;
90
- const clients = this.streamClients.get(key);
91
-
92
- if (clients && clients.length > 0) {
93
- const message = `data: ${JSON.stringify({ type, data, timestamp: Date.now() })}
94
-
95
- `;
96
- clients.forEach(client => {
97
- try {
98
- client.write(message);
99
- } catch (e) {
100
- console.error('Stream write error:', e);
101
- }
102
- });
103
- }
104
- }
105
-
106
- async executeCommand(sandboxId, command, args = [], options = {}) {
107
- const sandbox = this.sandboxes.get(sandboxId);
108
- if (!sandbox) throw new Error('Sandbox not found');
109
-
110
- sandbox.lastActivity = Date.now();
111
-
112
- const { background = false, timeout = 300000 } = options;
113
- const fullCommand = `${command} ${args.join(' ')}`;
114
-
115
- let isolatedCommand;
116
-
117
- if (sandbox.vm && this.useFirecracker) {
118
- // Execute in Firecracker VM
119
- console.log(`🔥 Executing in Firecracker: ${command}`);
120
- // This would route through Firecracker API
121
- // For now, fallback to direct execution
122
- isolatedCommand = `sh -c "cd ${sandbox.workspacePath} && ${fullCommand}"`;
123
- } else {
124
- // Direct execution with resource limits
125
- isolatedCommand = `sh -c "cd ${sandbox.workspacePath} && ulimit -t 300 -v 1048576 && timeout 300s ${fullCommand}"`;
126
- }
127
-
128
- if (background) {
129
- return this.startBackgroundProcess(sandboxId, isolatedCommand, sandbox.path, sandbox.port);
130
- } else {
131
- return this.startStreamingCommand(sandboxId, isolatedCommand, sandbox.path, timeout);
132
- }
133
- }
134
-
135
- startStreamingCommand(sandboxId, command, cwd, timeout) {
136
- return new Promise((resolve, reject) => {
137
- const cmdId = `cmd-${Date.now()}`;
138
- const dataKey = `${sandboxId}:${cmdId}`;
139
-
140
- this.commandData.set(dataKey, {
141
- stdout: '',
142
- stderr: '',
143
- exitCode: null,
144
- startedAt: Date.now()
145
- });
146
-
147
- const proc = spawn('sh', ['-c', command], {
148
- cwd,
149
- stdio: ['ignore', 'pipe', 'pipe'],
150
- env: {
151
- ...process.env,
152
- NODE_ENV: 'sandbox',
153
- PYTHONUNBUFFERED: '1'
154
- }
155
- });
156
-
157
- let completed = false;
158
-
159
- const timeoutId = setTimeout(() => {
160
- if (!completed) {
161
- proc.kill('SIGTERM');
162
- this.sendToStreamClients(sandboxId, cmdId, 'error', 'Command timeout');
163
- }
164
- }, timeout);
165
-
166
- proc.stdout.on('data', (data) => {
167
- const output = data.toString();
168
- const cmdData = this.commandData.get(dataKey);
169
- if (cmdData) cmdData.stdout += output;
170
-
171
- console.log(`[${sandboxId}][stdout]: ${output.trim()}`);
172
- this.sendToStreamClients(sandboxId, cmdId, 'stdout', output);
173
- });
174
-
175
- proc.stderr.on('data', (data) => {
176
- const output = data.toString();
177
- const cmdData = this.commandData.get(dataKey);
178
- if (cmdData) cmdData.stderr += output;
179
-
180
- console.log(`[${sandboxId}][stderr]: ${output.trim()}`);
181
- this.sendToStreamClients(sandboxId, cmdId, 'stderr', output);
182
- });
183
-
184
- proc.on('exit', (code) => {
185
- completed = true;
186
- clearTimeout(timeoutId);
187
-
188
- const cmdData = this.commandData.get(dataKey);
189
- if (cmdData) cmdData.exitCode = code;
190
-
191
- console.log(`[${sandboxId}] Command exited with code: ${code}`);
192
-
193
- this.sendToStreamClients(sandboxId, cmdId, 'complete', {
194
- exitCode: code,
195
- stdout: cmdData?.stdout || '',
196
- stderr: cmdData?.stderr || ''
197
- });
198
-
199
- resolve({
200
- commandId: cmdId,
201
- exitCode: code,
202
- stdout: cmdData?.stdout || '',
203
- stderr: cmdData?.stderr || '',
204
- streaming: true
205
- });
206
-
207
- setTimeout(() => this.commandData.delete(dataKey), 5 * 60 * 1000);
208
- });
209
-
210
- proc.on('error', (error) => {
211
- completed = true;
212
- clearTimeout(timeoutId);
213
- this.sendToStreamClients(sandboxId, cmdId, 'error', error.message);
214
- reject(error);
215
- });
216
-
217
- setTimeout(() => {
218
- if (!completed) {
219
- resolve({
220
- commandId: cmdId,
221
- status: 'streaming',
222
- message: 'Command started, streaming output'
223
- });
224
- }
225
- }, 100);
226
- });
227
- }
228
-
229
- startBackgroundProcess(sandboxId, command, cwd, port) {
230
- return new Promise((resolve, reject) => {
231
- const cmdId = `cmd-${Date.now()}`;
232
-
233
- const proc = spawn('sh', ['-c', command], {
234
- cwd,
235
- detached: true,
236
- stdio: ['ignore', 'pipe', 'pipe'],
237
- env: {
238
- ...process.env,
239
- PORT: port.toString(),
240
- NODE_ENV: 'development'
241
- }
242
- });
243
-
244
- proc.unref();
245
-
246
- let stdout = '';
247
- let stderr = '';
248
-
249
- proc.stdout.on('data', (data) => {
250
- stdout += data.toString();
251
- console.log(`[${sandboxId}][stdout]: ${data.toString().trim()}`);
252
- });
253
-
254
- proc.stderr.on('data', (data) => {
255
- stderr += data.toString();
256
- console.log(`[${sandboxId}][stderr]: ${data.toString().trim()}`);
257
- });
258
-
259
- this.processes.set(sandboxId, {
260
- cmdId,
261
- process: proc,
262
- port,
263
- stdout,
264
- stderr,
265
- startedAt: Date.now()
266
- });
267
-
268
- setTimeout(() => {
269
- if (proc.exitCode !== null) {
270
- reject(new Error('Process exited immediately'));
271
- } else {
272
- resolve({
273
- commandId: cmdId,
274
- status: 'running',
275
- pid: proc.pid,
276
- port
277
- });
278
- }
279
- }, 2000);
280
- });
281
- }
282
-
283
- async writeFiles(sandboxId, files) {
284
- const sandbox = this.sandboxes.get(sandboxId);
285
- if (!sandbox) throw new Error('Sandbox not found');
286
-
287
- sandbox.lastActivity = Date.now();
288
-
289
- for (const file of files) {
290
- const filePath = file.path;
291
- let fileContent = file.content;
292
-
293
- if (typeof fileContent === 'object' && fileContent !== null) {
294
- fileContent = JSON.stringify(fileContent, null, 2);
295
- }
296
-
297
- if (typeof fileContent !== 'string') {
298
- fileContent = String(fileContent);
299
- }
300
-
301
- const fullPath = path.join(sandbox.workspacePath, filePath);
302
- const dir = path.dirname(fullPath);
303
-
304
- await fs.mkdir(dir, { recursive: true });
305
- await fs.writeFile(fullPath, fileContent, 'utf-8');
306
-
307
- console.log(`📝 File written: ${filePath}`);
308
- }
309
- }
310
-
311
- async readFile(sandboxId, filePath) {
312
- const sandbox = this.sandboxes.get(sandboxId);
313
- if (!sandbox) throw new Error('Sandbox not found');
314
-
315
- const fullPath = path.join(sandbox.workspacePath, filePath);
316
- const content = await fs.readFile(fullPath, 'utf-8');
317
-
318
- return content;
319
- }
320
-
321
- async destroySandbox(sandboxId) {
322
- const sandbox = this.sandboxes.get(sandboxId);
323
- if (!sandbox) return;
324
-
325
- console.log(`🧹 Destroying sandbox: ${sandboxId}`);
326
-
327
- // Stop Firecracker VM if exists
328
- if (sandbox.vm) {
329
- await this.firecrackerManager.stopVM(sandboxId);
330
- }
331
-
332
- const procInfo = this.processes.get(sandboxId);
333
- if (procInfo) {
334
- try {
335
- process.kill(-procInfo.process.pid, 'SIGTERM');
336
- this.processes.delete(sandboxId);
337
- } catch (e) {
338
- console.error('Process kill error:', e.message);
339
- }
340
- }
341
-
342
- try {
343
- await fs.rm(sandbox.path, { recursive: true, force: true });
344
- } catch (e) {
345
- console.error('File cleanup error:', e.message);
346
- }
347
-
348
- this.sandboxes.delete(sandboxId);
349
- console.log(`✅ Sandbox destroyed: ${sandboxId}`);
350
- }
351
-
352
- getSandbox(sandboxId) {
353
- return this.sandboxes.get(sandboxId);
354
- }
355
-
356
- getProcess(sandboxId) {
357
- return this.processes.get(sandboxId);
358
- }
359
-
360
- async cleanupInactive() {
361
- const now = Date.now();
362
-
363
- for (const [id, sandbox] of this.sandboxes.entries()) {
364
- const inactive = (now - sandbox.lastActivity) > (15 * 60 * 1000);
365
- const timedOut = (now - sandbox.createdAt) > sandbox.timeout;
366
-
367
- if (inactive || timedOut) {
368
- console.log(`⏰ Cleaning up sandbox: ${id} (${timedOut ? 'timeout' : 'inactive'})`);
369
- await this.destroySandbox(id);
370
- }
371
- }
372
- }
373
- }
374
-
375
- module.exports = SimpleSandboxManager;
 
1
+ async executeCommand(sandboxId, command, args = [], options = {}) {
2
+ const sandbox = this.sandboxes.get(sandboxId);
3
+ if (!sandbox) throw new Error('Sandbox not found');
4
+
5
+ sandbox.lastActivity = Date.now();
6
+
7
+ const { background = false, timeout = 300000 } = options;
8
+ const fullCommand = `${command} ${args.join(' ')}`;
9
+
10
+ // Pragmatic approach - resource limits without complex isolation
11
+ // ulimit: limit CPU time (300s), virtual memory (1GB), file size (1GB)
12
+ // timeout: additional safety net
13
+ const isolatedCommand = `sh -c "
14
+ cd ${sandbox.workspacePath} && \\
15
+ ulimit -t 300 -v 1048576 -f 1048576 && \\
16
+ timeout 300s ${fullCommand}
17
+ "`;
18
+
19
+ if (background) {
20
+ return this.startBackgroundProcess(sandboxId, isolatedCommand, sandbox.path, sandbox.port);
21
+ } else {
22
+ return this.startStreamingCommand(sandboxId, isolatedCommand, sandbox.path, timeout);
23
+ }
24
+ }