RaBU1234 commited on
Commit
175235d
·
verified ·
1 Parent(s): d3855e1

Update simple-sandbox-manager.js

Browse files
Files changed (1) hide show
  1. simple-sandbox-manager.js +144 -165
simple-sandbox-manager.js CHANGED
@@ -1,5 +1,6 @@
1
  const { spawn, exec } = require('child_process');
2
  const fs = require('fs').promises;
 
3
  const path = require('path');
4
  const { EventEmitter } = require('events');
5
 
@@ -7,10 +8,10 @@ class SimpleSandboxManager extends EventEmitter {
7
  constructor() {
8
  super();
9
  this.sandboxes = new Map();
10
- this.processes = new Map();
 
11
  this.basePath = '/tmp/sandboxes';
12
  this.portCounter = 3000;
13
- this.streamClients = new Map();
14
  }
15
 
16
  async initialize() {
@@ -18,60 +19,81 @@ class SimpleSandboxManager extends EventEmitter {
18
  console.log('✅ Simple Sandbox Manager initialized');
19
  }
20
 
21
- async createSandbox(sandboxId) {
 
22
  const sandboxPath = path.join(this.basePath, sandboxId);
23
  await fs.mkdir(sandboxPath, { recursive: true });
24
 
25
  const workspacePath = path.join(sandboxPath, 'workspace');
26
  await fs.mkdir(workspacePath, { recursive: true });
27
 
28
- const port = this.portCounter++;
29
 
30
  const sandbox = {
31
  sandboxId,
32
  path: sandboxPath,
33
  workspacePath,
34
- port,
35
  createdAt: Date.now(),
36
  lastActivity: Date.now(),
37
- status: 'ready'
 
38
  };
39
 
40
  this.sandboxes.set(sandboxId, sandbox);
41
- console.log(`✅ Sandbox created: ${sandboxId} (port: ${port})`);
 
 
 
 
 
 
 
42
 
43
  return sandbox;
44
  }
45
 
46
- addStreamClient(sandboxId, commandId, res) {
47
- const key = `${sandboxId}:${commandId}`;
48
- if (!this.streamClients.has(key)) {
49
- this.streamClients.set(key, []);
 
50
  }
51
- this.streamClients.get(key).push(res);
 
 
 
 
 
 
 
52
 
53
- res.on('close', () => {
54
- const clients = this.streamClients.get(key);
55
- if (clients) {
56
- const index = clients.indexOf(res);
57
- if (index > -1) clients.splice(index, 1);
58
  }
59
  });
60
  }
61
 
62
- sendToStreamClients(sandboxId, commandId, type, data) {
63
- const key = `${sandboxId}:${commandId}`;
64
- const clients = this.streamClients.get(key);
65
 
66
- if (clients && clients.length > 0) {
67
- const message = `data: ${JSON.stringify({ type, data, timestamp: Date.now() })}
68
-
69
- `;
70
- clients.forEach(client => {
 
 
 
 
71
  try {
72
- client.write(message);
73
  } catch (e) {
74
- console.error('Stream write error:', e);
75
  }
76
  });
77
  }
@@ -81,12 +103,16 @@ class SimpleSandboxManager extends EventEmitter {
81
  const sandbox = this.sandboxes.get(sandboxId);
82
  if (!sandbox) throw new Error('Sandbox not found');
83
 
 
 
 
 
84
  sandbox.lastActivity = Date.now();
85
 
86
- const { background = false, timeout = 300000, stream = false } = options;
87
 
88
  const allowedCommands = [
89
- 'node', 'npm', 'npx', 'python3', 'python', 'pip',
90
  'go', 'cat', 'ls', 'mkdir', 'echo', 'touch',
91
  'rm', 'cp', 'mv', 'pwd', 'which', 'sh', 'bash', 'chmod'
92
  ];
@@ -96,133 +122,73 @@ class SimpleSandboxManager extends EventEmitter {
96
  throw new Error(`Command not allowed: ${baseCommand}`);
97
  }
98
 
99
- const fullCommand = `${command} ${args.join(' ')}`;
100
 
101
- if (background) {
102
- return this.startBackgroundProcess(sandboxId, fullCommand, sandbox.workspacePath, sandbox.port);
103
- } else if (stream) {
104
- return this.startStreamingCommand(sandboxId, fullCommand, sandbox.workspacePath, timeout);
105
  } else {
106
- return this.runSyncCommand(fullCommand, sandbox.workspacePath, timeout);
107
  }
108
  }
109
 
110
- startStreamingCommand(sandboxId, command, cwd, timeout) {
111
  return new Promise((resolve, reject) => {
112
- const cmdId = `cmd-${Date.now()}`;
 
113
 
114
  const proc = spawn('sh', ['-c', command], {
115
  cwd,
 
116
  stdio: ['ignore', 'pipe', 'pipe'],
117
  env: {
118
  ...process.env,
119
- NODE_ENV: 'sandbox',
120
- PYTHONUNBUFFERED: '1'
121
  }
122
  });
123
 
124
- let stdout = '';
125
- let stderr = '';
126
- let completed = false;
127
 
128
- const timeoutId = setTimeout(() => {
129
- if (!completed) {
130
- proc.kill('SIGTERM');
131
- this.sendToStreamClients(sandboxId, cmdId, 'error', 'Command timeout');
132
- }
133
- }, timeout);
 
 
 
 
 
 
 
134
 
135
  proc.stdout.on('data', (data) => {
136
  const output = data.toString();
137
- stdout += output;
138
- console.log(`[${sandboxId}][stdout]: ${output.trim()}`);
139
- this.sendToStreamClients(sandboxId, cmdId, 'stdout', output);
 
140
  });
141
 
142
  proc.stderr.on('data', (data) => {
143
  const output = data.toString();
144
- stderr += output;
145
- console.log(`[${sandboxId}][stderr]: ${output.trim()}`);
146
- this.sendToStreamClients(sandboxId, cmdId, 'stderr', output);
 
147
  });
148
 
149
  proc.on('exit', (code) => {
150
- completed = true;
151
- clearTimeout(timeoutId);
152
-
153
- console.log(`[${sandboxId}] Command exited with code: ${code}`);
154
-
155
- this.sendToStreamClients(sandboxId, cmdId, 'complete', {
156
- exitCode: code,
157
- stdout,
158
- stderr
159
- });
160
-
161
- resolve({
162
- commandId: cmdId,
163
- exitCode: code,
164
- stdout,
165
- stderr,
166
- streaming: true
167
- });
168
  });
169
 
170
  proc.on('error', (error) => {
171
- completed = true;
172
- clearTimeout(timeoutId);
173
- this.sendToStreamClients(sandboxId, cmdId, 'error', error.message);
174
- reject(error);
175
- });
176
-
177
- setTimeout(() => {
178
- if (!completed) {
179
- resolve({
180
- commandId: cmdId,
181
- status: 'streaming',
182
- message: 'Command started, streaming output'
183
- });
184
- }
185
- }, 100);
186
- });
187
- }
188
-
189
- startBackgroundProcess(sandboxId, command, cwd, port) {
190
- return new Promise((resolve, reject) => {
191
- const cmdId = `cmd-${Date.now()}`;
192
-
193
- const proc = spawn('sh', ['-c', command], {
194
- cwd,
195
- detached: true,
196
- stdio: ['ignore', 'pipe', 'pipe'],
197
- env: {
198
- ...process.env,
199
- PORT: port.toString(),
200
- NODE_ENV: 'development'
201
- }
202
- });
203
-
204
- proc.unref();
205
-
206
- let stdout = '';
207
- let stderr = '';
208
-
209
- proc.stdout.on('data', (data) => {
210
- stdout += data.toString();
211
- console.log(`[${sandboxId}][stdout]: ${data.toString().trim()}`);
212
- });
213
-
214
- proc.stderr.on('data', (data) => {
215
- stderr += data.toString();
216
- console.log(`[${sandboxId}][stderr]: ${data.toString().trim()}`);
217
- });
218
-
219
- this.processes.set(sandboxId, {
220
- cmdId,
221
- process: proc,
222
- port,
223
- stdout,
224
- stderr,
225
- startedAt: Date.now()
226
  });
227
 
228
  setTimeout(() => {
@@ -230,29 +196,56 @@ class SimpleSandboxManager extends EventEmitter {
230
  reject(new Error('Process exited immediately'));
231
  } else {
232
  resolve({
 
233
  commandId: cmdId,
234
  status: 'running',
235
  pid: proc.pid,
236
  port
237
  });
238
  }
239
- }, 2000);
240
  });
241
  }
242
 
243
- runSyncCommand(command, cwd, timeout) {
244
  return new Promise((resolve, reject) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  exec(command, {
246
  cwd,
247
- timeout,
248
  maxBuffer: 10 * 1024 * 1024,
249
  env: {
250
  ...process.env,
251
  NODE_ENV: 'sandbox'
252
  }
253
  }, (error, stdout, stderr) => {
 
 
 
 
 
 
 
 
 
254
  resolve({
255
- exitCode: error ? (error.code || 1) : 0,
 
 
256
  stdout: stdout.toString(),
257
  stderr: stderr.toString()
258
  });
@@ -260,21 +253,8 @@ class SimpleSandboxManager extends EventEmitter {
260
  });
261
  }
262
 
263
- async stopCommand(sandboxId) {
264
- const procInfo = this.processes.get(sandboxId);
265
- if (!procInfo) {
266
- throw new Error('No running process found');
267
- }
268
-
269
- try {
270
- process.kill(-procInfo.process.pid, 'SIGTERM');
271
- this.processes.delete(sandboxId);
272
- console.log(`🛑 Process stopped for ${sandboxId}`);
273
- return { stopped: true };
274
- } catch (e) {
275
- console.error('Failed to stop process:', e);
276
- throw e;
277
- }
278
  }
279
 
280
  async writeFiles(sandboxId, files) {
@@ -304,18 +284,18 @@ class SimpleSandboxManager extends EventEmitter {
304
  return content;
305
  }
306
 
307
- async listFiles(sandboxId, dirPath = '.') {
308
  const sandbox = this.sandboxes.get(sandboxId);
309
  if (!sandbox) throw new Error('Sandbox not found');
310
 
311
- const fullPath = path.join(sandbox.workspacePath, dirPath);
312
- const files = await fs.readdir(fullPath, { withFileTypes: true });
313
 
314
- return files.map(file => ({
315
- name: file.name,
316
- isDirectory: file.isDirectory(),
317
- path: path.join(dirPath, file.name)
318
- }));
 
319
  }
320
 
321
  async destroySandbox(sandboxId) {
@@ -324,13 +304,16 @@ class SimpleSandboxManager extends EventEmitter {
324
 
325
  console.log(`🧹 Destroying sandbox: ${sandboxId}`);
326
 
327
- const procInfo = this.processes.get(sandboxId);
328
- if (procInfo) {
329
- try {
330
- process.kill(-procInfo.process.pid, 'SIGTERM');
331
- this.processes.delete(sandboxId);
332
- } catch (e) {
333
- console.error('Process kill error:', e.message);
 
 
 
334
  }
335
  }
336
 
@@ -348,10 +331,6 @@ class SimpleSandboxManager extends EventEmitter {
348
  return this.sandboxes.get(sandboxId);
349
  }
350
 
351
- getProcess(sandboxId) {
352
- return this.processes.get(sandboxId);
353
- }
354
-
355
  async cleanupInactive(inactiveTimeout = 15 * 60 * 1000) {
356
  const now = Date.now();
357
 
 
1
  const { spawn, exec } = require('child_process');
2
  const fs = require('fs').promises;
3
+ const fsSync = require('fs');
4
  const path = require('path');
5
  const { EventEmitter } = require('events');
6
 
 
8
  constructor() {
9
  super();
10
  this.sandboxes = new Map();
11
+ this.commands = new Map();
12
+ this.logStreams = new Map();
13
  this.basePath = '/tmp/sandboxes';
14
  this.portCounter = 3000;
 
15
  }
16
 
17
  async initialize() {
 
19
  console.log('✅ Simple Sandbox Manager initialized');
20
  }
21
 
22
+ async createSandbox(sandboxId, options = {}) {
23
+ const { timeout = 600000, ports = [] } = options;
24
  const sandboxPath = path.join(this.basePath, sandboxId);
25
  await fs.mkdir(sandboxPath, { recursive: true });
26
 
27
  const workspacePath = path.join(sandboxPath, 'workspace');
28
  await fs.mkdir(workspacePath, { recursive: true });
29
 
30
+ const assignedPorts = ports.length > 0 ? ports : [this.portCounter++];
31
 
32
  const sandbox = {
33
  sandboxId,
34
  path: sandboxPath,
35
  workspacePath,
36
+ ports: assignedPorts,
37
  createdAt: Date.now(),
38
  lastActivity: Date.now(),
39
+ status: 'ready',
40
+ timeout
41
  };
42
 
43
  this.sandboxes.set(sandboxId, sandbox);
44
+
45
+ if (timeout > 0) {
46
+ setTimeout(() => {
47
+ this.stopSandbox(sandboxId);
48
+ }, timeout);
49
+ }
50
+
51
+ console.log(`✅ Sandbox created: ${sandboxId} (ports: ${assignedPorts.join(', ')})`);
52
 
53
  return sandbox;
54
  }
55
 
56
+ async stopSandbox(sandboxId) {
57
+ const sandbox = this.sandboxes.get(sandboxId);
58
+ if (sandbox && sandbox.status !== 'stopped') {
59
+ sandbox.status = 'stopped';
60
+ console.log(`⏹️ Sandbox stopped: ${sandboxId}`);
61
  }
62
+ }
63
+
64
+ addLogStream(sandboxId, cmdId, stream) {
65
+ const key = `${sandboxId}:${cmdId}`;
66
+ if (!this.logStreams.has(key)) {
67
+ this.logStreams.set(key, []);
68
+ }
69
+ this.logStreams.get(key).push(stream);
70
 
71
+ stream.on('close', () => {
72
+ const streams = this.logStreams.get(key);
73
+ if (streams) {
74
+ const index = streams.indexOf(stream);
75
+ if (index > -1) streams.splice(index, 1);
76
  }
77
  });
78
  }
79
 
80
+ sendToLogStreams(sandboxId, cmdId, data, streamType) {
81
+ const key = `${sandboxId}:${cmdId}`;
82
+ const streams = this.logStreams.get(key);
83
 
84
+ if (streams && streams.length > 0) {
85
+ const logEntry = JSON.stringify({
86
+ data,
87
+ stream: streamType,
88
+ timestamp: Date.now()
89
+ }) + '
90
+ ';
91
+
92
+ streams.forEach(stream => {
93
  try {
94
+ stream.write(logEntry);
95
  } catch (e) {
96
+ console.error('Log stream write error:', e);
97
  }
98
  });
99
  }
 
103
  const sandbox = this.sandboxes.get(sandboxId);
104
  if (!sandbox) throw new Error('Sandbox not found');
105
 
106
+ if (sandbox.status === 'stopped') {
107
+ throw new Error('Sandbox has been stopped');
108
+ }
109
+
110
  sandbox.lastActivity = Date.now();
111
 
112
+ const { wait = true, sudo = false, background = false } = options;
113
 
114
  const allowedCommands = [
115
+ 'node', 'npm', 'npx', 'pnpm', 'yarn', 'python3', 'python', 'pip',
116
  'go', 'cat', 'ls', 'mkdir', 'echo', 'touch',
117
  'rm', 'cp', 'mv', 'pwd', 'which', 'sh', 'bash', 'chmod'
118
  ];
 
122
  throw new Error(`Command not allowed: ${baseCommand}`);
123
  }
124
 
125
+ const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;
126
 
127
+ if (background || !wait) {
128
+ return this.startBackgroundCommand(sandboxId, fullCommand, sandbox.workspacePath, sandbox.ports[0]);
 
 
129
  } else {
130
+ return this.runSyncCommand(sandboxId, fullCommand, sandbox.workspacePath);
131
  }
132
  }
133
 
134
+ startBackgroundCommand(sandboxId, command, cwd, port) {
135
  return new Promise((resolve, reject) => {
136
+ const cmdId = `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
137
+ const startedAt = Date.now();
138
 
139
  const proc = spawn('sh', ['-c', command], {
140
  cwd,
141
+ detached: true,
142
  stdio: ['ignore', 'pipe', 'pipe'],
143
  env: {
144
  ...process.env,
145
+ PORT: port.toString(),
146
+ NODE_ENV: 'development'
147
  }
148
  });
149
 
150
+ proc.unref();
 
 
151
 
152
+ const commandInfo = {
153
+ cmdId,
154
+ sandboxId,
155
+ process: proc,
156
+ command,
157
+ port,
158
+ startedAt,
159
+ logs: [],
160
+ status: 'running',
161
+ exitCode: null
162
+ };
163
+
164
+ this.commands.set(`${sandboxId}:${cmdId}`, commandInfo);
165
 
166
  proc.stdout.on('data', (data) => {
167
  const output = data.toString();
168
+ const log = { data: output, stream: 'stdout', timestamp: Date.now() };
169
+ commandInfo.logs.push(log);
170
+ this.sendToLogStreams(sandboxId, cmdId, output, 'stdout');
171
+ console.log(`[${sandboxId}][${cmdId}][stdout]: ${output.trim()}`);
172
  });
173
 
174
  proc.stderr.on('data', (data) => {
175
  const output = data.toString();
176
+ const log = { data: output, stream: 'stderr', timestamp: Date.now() };
177
+ commandInfo.logs.push(log);
178
+ this.sendToLogStreams(sandboxId, cmdId, output, 'stderr');
179
+ console.log(`[${sandboxId}][${cmdId}][stderr]: ${output.trim()}`);
180
  });
181
 
182
  proc.on('exit', (code) => {
183
+ commandInfo.exitCode = code;
184
+ commandInfo.status = 'done';
185
+ console.log(`[${sandboxId}][${cmdId}] Process exited with code: ${code}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  });
187
 
188
  proc.on('error', (error) => {
189
+ commandInfo.status = 'error';
190
+ commandInfo.exitCode = 1;
191
+ console.error(`[${sandboxId}][${cmdId}] Error:`, error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  });
193
 
194
  setTimeout(() => {
 
196
  reject(new Error('Process exited immediately'));
197
  } else {
198
  resolve({
199
+ cmdId,
200
  commandId: cmdId,
201
  status: 'running',
202
  pid: proc.pid,
203
  port
204
  });
205
  }
206
+ }, 1000);
207
  });
208
  }
209
 
210
+ runSyncCommand(sandboxId, command, cwd) {
211
  return new Promise((resolve, reject) => {
212
+ const cmdId = `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
213
+ const startedAt = Date.now();
214
+
215
+ const commandInfo = {
216
+ cmdId,
217
+ sandboxId,
218
+ command,
219
+ startedAt,
220
+ logs: [],
221
+ status: 'executing',
222
+ exitCode: null
223
+ };
224
+
225
+ this.commands.set(`${sandboxId}:${cmdId}`, commandInfo);
226
+
227
  exec(command, {
228
  cwd,
229
+ timeout: 300000,
230
  maxBuffer: 10 * 1024 * 1024,
231
  env: {
232
  ...process.env,
233
  NODE_ENV: 'sandbox'
234
  }
235
  }, (error, stdout, stderr) => {
236
+ const exitCode = error ? (error.code || 1) : 0;
237
+
238
+ commandInfo.exitCode = exitCode;
239
+ commandInfo.status = 'done';
240
+ commandInfo.logs.push(
241
+ { data: stdout, stream: 'stdout', timestamp: Date.now() },
242
+ { data: stderr, stream: 'stderr', timestamp: Date.now() }
243
+ );
244
+
245
  resolve({
246
+ cmdId,
247
+ commandId: cmdId,
248
+ exitCode,
249
  stdout: stdout.toString(),
250
  stderr: stderr.toString()
251
  });
 
253
  });
254
  }
255
 
256
+ getCommand(sandboxId, cmdId) {
257
+ return this.commands.get(`${sandboxId}:${cmdId}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  }
259
 
260
  async writeFiles(sandboxId, files) {
 
284
  return content;
285
  }
286
 
287
+ async readFileStream(sandboxId, filePath) {
288
  const sandbox = this.sandboxes.get(sandboxId);
289
  if (!sandbox) throw new Error('Sandbox not found');
290
 
291
+ const fullPath = path.join(sandbox.workspacePath, filePath);
 
292
 
293
+ try {
294
+ await fs.access(fullPath);
295
+ return fsSync.createReadStream(fullPath);
296
+ } catch (e) {
297
+ return null;
298
+ }
299
  }
300
 
301
  async destroySandbox(sandboxId) {
 
304
 
305
  console.log(`🧹 Destroying sandbox: ${sandboxId}`);
306
 
307
+ for (const [key, cmd] of this.commands.entries()) {
308
+ if (key.startsWith(`${sandboxId}:`)) {
309
+ if (cmd.process) {
310
+ try {
311
+ process.kill(-cmd.process.pid, 'SIGTERM');
312
+ } catch (e) {
313
+ console.error('Process kill error:', e.message);
314
+ }
315
+ }
316
+ this.commands.delete(key);
317
  }
318
  }
319
 
 
331
  return this.sandboxes.get(sandboxId);
332
  }
333
 
 
 
 
 
334
  async cleanupInactive(inactiveTimeout = 15 * 60 * 1000) {
335
  const now = Date.now();
336