RaBU1234 commited on
Commit
88510eb
·
verified ·
1 Parent(s): c904528

Update simple-sandbox-manager.js

Browse files
Files changed (1) hide show
  1. simple-sandbox-manager.js +55 -109
simple-sandbox-manager.js CHANGED
@@ -1,23 +1,19 @@
1
- // simple-sandbox-manager.js
2
-
3
  const { spawn } = require('child_process');
4
  const fs = require('fs').promises;
5
  const path = require('path');
6
- const { EventEmitter } = require('events');
7
 
8
- class SimpleSandboxManager extends EventEmitter {
9
  constructor() {
10
- super();
11
  this.sandboxes = new Map();
12
- this.basePath = path.join('/tmp', 'sandboxes'); // Use path.join for cross-OS compatibility
13
- this.portCounter = 9000; // Start from a higher port range
14
  this.streamClients = new Map();
15
  this.commandData = new Map();
16
  }
17
 
18
  async initialize() {
19
  await fs.mkdir(this.basePath, { recursive: true });
20
- console.log('✅ Sandbox Manager initialized');
21
  }
22
 
23
  async createSandbox(sandboxId, options = {}) {
@@ -34,16 +30,11 @@ class SimpleSandboxManager extends EventEmitter {
34
  timeout,
35
  createdAt: Date.now(),
36
  lastActivity: Date.now(),
37
- status: 'ready',
38
- url: null, // To be set when a service starts
39
  };
40
 
41
  this.sandboxes.set(sandboxId, sandbox);
42
- // Associate port with sandbox for URL lookup
43
- // This is a simple way to assign a URL. In a real scenario, a proxy manager would handle this.
44
- sandbox.url = `http://localhost:${port}`;
45
- console.log(`✅ Sandbox created: ${sandboxId} at ${sandboxPath} (URL: ${sandbox.url})`);
46
-
47
  return sandbox;
48
  }
49
 
@@ -53,87 +44,66 @@ class SimpleSandboxManager extends EventEmitter {
53
  return (Date.now() - sandbox.createdAt) > sandbox.timeout;
54
  }
55
 
56
- addStreamClient(sandboxId, commandId, res) {
57
- const key = `${sandboxId}:${commandId}`;
58
- if (!this.streamClients.has(key)) {
59
- this.streamClients.set(key, []);
60
- }
61
  this.streamClients.get(key).push(res);
62
  }
63
 
64
- removeStreamClient(sandboxId, commandId, res) {
65
- const key = `${sandboxId}:${commandId}`;
66
- const clients = this.streamClients.get(key);
67
- if (clients) {
68
- const index = clients.indexOf(res);
69
- if (index > -1) clients.splice(index, 1);
70
- if (clients.length === 0) {
71
- this.streamClients.delete(key);
72
- }
73
- }
74
  }
75
 
76
- sendToStreamClients(sandboxId, commandId, type, data) {
77
- const key = `${sandboxId}:${commandId}`;
78
  const clients = this.streamClients.get(key);
79
-
80
- if (clients && clients.length > 0) {
81
- const message = `data: ${JSON.stringify({ type, data, timestamp: Date.now() })}\n\n`;
82
- clients.forEach(client => {
83
- try {
84
- client.write(message);
85
- } catch (e) {
86
- console.error('Stream write error:', e.message);
87
- // Client probably disconnected, remove them
88
- this.removeStreamClient(sandboxId, commandId, client);
89
- }
90
- });
91
- }
92
  }
93
 
94
- async executeCommand(sandboxId, command, options = {}) {
95
  const sandbox = this.sandboxes.get(sandboxId);
96
  if (!sandbox) throw new Error('Sandbox not found');
97
-
98
  sandbox.lastActivity = Date.now();
99
-
100
- const { background = false, timeout = 300000 } = options;
101
-
102
- // Command is executed inside the sandbox's path
103
  const proc = spawn(command, [], {
104
  cwd: sandbox.path,
105
- shell: true, // Use shell to interpret the command string
106
  stdio: ['ignore', 'pipe', 'pipe'],
107
- env: {
108
- ...process.env,
109
- PORT: sandbox.port.toString(), // Inject PORT for services like `npm start`
110
- NODE_ENV: 'development',
111
- PYTHONUNBUFFERED: '1',
112
- }
113
  });
114
 
115
- const cmdId = `cmd-${proc.pid}-${Date.now()}`;
116
  const dataKey = `${sandboxId}:${cmdId}`;
117
 
118
- this.commandData.set(dataKey, {
119
- stdout: '',
120
- stderr: '',
121
- exitCode: null,
122
- startedAt: Date.now()
123
- });
124
 
125
  console.log(`[${sandboxId}] CMD (${cmdId}): ${command}`);
126
 
127
  proc.stdout.on('data', (data) => {
128
- const output = data.toString();
129
- this.commandData.get(dataKey).stdout += output;
130
- this.sendToStreamClients(sandboxId, cmdId, 'stdout', output);
131
  });
132
 
133
  proc.stderr.on('data', (data) => {
134
- const output = data.toString();
135
- this.commandData.get(dataKey).stderr += output;
136
- this.sendToStreamClients(sandboxId, cmdId, 'stderr', output);
137
  });
138
 
139
  proc.on('exit', (code) => {
@@ -141,20 +111,19 @@ class SimpleSandboxManager extends EventEmitter {
141
  const cmdData = this.commandData.get(dataKey);
142
  if (cmdData) cmdData.exitCode = code;
143
  this.sendToStreamClients(sandboxId, cmdId, 'complete', { exitCode: code });
144
- // Clean up old command data after a while
145
- setTimeout(() => this.commandData.delete(dataKey), 5 * 60 * 1000);
146
  });
147
 
148
- proc.on('error', (error) => {
149
- console.error(`[${sandboxId}] CMD (${cmdId}) error:`, error);
150
- this.sendToStreamClients(sandboxId, cmdId, 'error', error.message);
151
  });
152
 
153
- return {
154
- commandId: cmdId,
155
- status: 'running',
156
- streaming: true
157
- };
158
  }
159
 
160
  async writeFile(sandboxId, filePath, content) {
@@ -163,26 +132,14 @@ class SimpleSandboxManager extends EventEmitter {
163
  sandbox.lastActivity = Date.now();
164
 
165
  const fullPath = path.join(sandbox.path, filePath);
166
- const dir = path.dirname(fullPath);
167
-
168
- await fs.mkdir(dir, { recursive: true });
169
  await fs.writeFile(fullPath, content, 'utf-8');
170
-
171
  console.log(`📝 File written to ${sandboxId}: ${filePath}`);
172
  }
173
 
174
- async readFile(sandboxId, filePath) {
175
- const sandbox = this.sandboxes.get(sandboxId);
176
- if (!sandbox) throw new Error('Sandbox not found');
177
-
178
- const fullPath = path.join(sandbox.path, filePath);
179
- return fs.readFile(fullPath, 'utf-8');
180
- }
181
-
182
  async getURL(sandboxId) {
183
  const sandbox = this.sandboxes.get(sandboxId);
184
  if (!sandbox) throw new Error('Sandbox not found');
185
- if (!sandbox.url) throw new Error('URL not available for this sandbox');
186
  return sandbox.url;
187
  }
188
 
@@ -191,25 +148,17 @@ class SimpleSandboxManager extends EventEmitter {
191
  if (!sandbox) return;
192
 
193
  console.log(`🧹 Destroying sandbox: ${sandboxId}`);
194
-
195
  this.sandboxes.delete(sandboxId);
196
 
197
  try {
198
- // The `force` option is deprecated, use `rm` which is equivalent
199
  await fs.rm(sandbox.path, { recursive: true, force: true });
200
  } catch (e) {
201
- // Ignore errors if path is already gone
202
- if (e.code !== 'ENOENT') {
203
- console.error(`File cleanup error for ${sandboxId}:`, e.message);
204
- }
205
  }
206
- console.log(`✅ Sandbox destroyed: ${sandboxId}`);
207
  }
208
 
209
  async destroyAllSandboxes() {
210
- for (const id of this.sandboxes.keys()) {
211
- await this.destroySandbox(id);
212
- }
213
  }
214
 
215
  getSandbox(sandboxId) {
@@ -219,11 +168,8 @@ class SimpleSandboxManager extends EventEmitter {
219
  async cleanupInactive() {
220
  const now = Date.now();
221
  for (const [id, sandbox] of this.sandboxes.entries()) {
222
- const inactive = (now - sandbox.lastActivity) > (15 * 60 * 1000); // 15 mins inactive
223
- const timedOut = (now - sandbox.createdAt) > sandbox.timeout;
224
-
225
- if (inactive || timedOut) {
226
- console.log(`⏰ Cleaning up sandbox: ${id} (${timedOut ? 'global timeout' : 'inactive'})`);
227
  await this.destroySandbox(id);
228
  }
229
  }
 
 
 
1
  const { spawn } = require('child_process');
2
  const fs = require('fs').promises;
3
  const path = require('path');
 
4
 
5
+ class SimpleSandboxManager {
6
  constructor() {
 
7
  this.sandboxes = new Map();
8
+ this.basePath = path.join('/tmp', 'sandboxes');
9
+ this.portCounter = 9000;
10
  this.streamClients = new Map();
11
  this.commandData = new Map();
12
  }
13
 
14
  async initialize() {
15
  await fs.mkdir(this.basePath, { recursive: true });
16
+ console.log('✅ Sandbox Manager initialized at:', this.basePath);
17
  }
18
 
19
  async createSandbox(sandboxId, options = {}) {
 
30
  timeout,
31
  createdAt: Date.now(),
32
  lastActivity: Date.now(),
33
+ url: `http://localhost:${port}`,
 
34
  };
35
 
36
  this.sandboxes.set(sandboxId, sandbox);
37
+ console.log(`✅ Sandbox created: ${sandboxId} | URL: ${sandbox.url}`);
 
 
 
 
38
  return sandbox;
39
  }
40
 
 
44
  return (Date.now() - sandbox.createdAt) > sandbox.timeout;
45
  }
46
 
47
+ addStreamClient(id, cmdId, res) {
48
+ const key = `${id}:${cmdId}`;
49
+ if (!this.streamClients.has(key)) this.streamClients.set(key, []);
 
 
50
  this.streamClients.get(key).push(res);
51
  }
52
 
53
+ removeStreamClient(id, cmdId, res) {
54
+ const key = `${id}:${cmdId}`;
55
+ const clients = this.streamClients.get(key);
56
+ if (clients) {
57
+ const index = clients.indexOf(res);
58
+ if (index > -1) clients.splice(index, 1);
59
+ if (clients.length === 0) this.streamClients.delete(key);
60
+ }
 
 
61
  }
62
 
63
+ sendToStreamClients(id, cmdId, type, data) {
64
+ const key = `${id}:${cmdId}`;
65
  const clients = this.streamClients.get(key);
66
+ if (!clients || clients.length === 0) return;
67
+
68
+ const message = `data: ${JSON.stringify({ type, data, timestamp: Date.now() })}\n\n`;
69
+ clients.forEach(client => {
70
+ try {
71
+ client.write(message);
72
+ } catch (e) {
73
+ this.removeStreamClient(id, cmdId, client);
74
+ }
75
+ });
 
 
 
76
  }
77
 
78
+ async executeCommand(sandboxId, command) {
79
  const sandbox = this.sandboxes.get(sandboxId);
80
  if (!sandbox) throw new Error('Sandbox not found');
 
81
  sandbox.lastActivity = Date.now();
82
+
 
 
 
83
  const proc = spawn(command, [], {
84
  cwd: sandbox.path,
85
+ shell: true,
86
  stdio: ['ignore', 'pipe', 'pipe'],
87
+ env: { ...process.env, PORT: sandbox.port.toString() },
 
 
 
 
 
88
  });
89
 
90
+ const cmdId = `cmd-${proc.pid}`;
91
  const dataKey = `${sandboxId}:${cmdId}`;
92
 
93
+ this.commandData.set(dataKey, { stdout: '', stderr: '', exitCode: null, startedAt: Date.now() });
 
 
 
 
 
94
 
95
  console.log(`[${sandboxId}] CMD (${cmdId}): ${command}`);
96
 
97
  proc.stdout.on('data', (data) => {
98
+ const out = data.toString();
99
+ this.commandData.get(dataKey).stdout += out;
100
+ this.sendToStreamClients(sandboxId, cmdId, 'stdout', out);
101
  });
102
 
103
  proc.stderr.on('data', (data) => {
104
+ const err = data.toString();
105
+ this.commandData.get(dataKey).stderr += err;
106
+ this.sendToStreamClients(sandboxId, cmdId, 'stderr', err);
107
  });
108
 
109
  proc.on('exit', (code) => {
 
111
  const cmdData = this.commandData.get(dataKey);
112
  if (cmdData) cmdData.exitCode = code;
113
  this.sendToStreamClients(sandboxId, cmdId, 'complete', { exitCode: code });
114
+ setTimeout(() => this.commandData.delete(dataKey), 10 * 60 * 1000); // 10 min cleanup
 
115
  });
116
 
117
+ proc.on('error', (err) => {
118
+ console.error(`[${sandboxId}] CMD (${cmdId}) error:`, err);
119
+ this.sendToStreamClients(sandboxId, cmdId, 'error', err.message);
120
  });
121
 
122
+ return { commandId: cmdId };
123
+ }
124
+
125
+ getCommandData(sandboxId, cmdId) {
126
+ return this.commandData.get(`${sandboxId}:${cmdId}`);
127
  }
128
 
129
  async writeFile(sandboxId, filePath, content) {
 
132
  sandbox.lastActivity = Date.now();
133
 
134
  const fullPath = path.join(sandbox.path, filePath);
135
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
 
 
136
  await fs.writeFile(fullPath, content, 'utf-8');
 
137
  console.log(`📝 File written to ${sandboxId}: ${filePath}`);
138
  }
139
 
 
 
 
 
 
 
 
 
140
  async getURL(sandboxId) {
141
  const sandbox = this.sandboxes.get(sandboxId);
142
  if (!sandbox) throw new Error('Sandbox not found');
 
143
  return sandbox.url;
144
  }
145
 
 
148
  if (!sandbox) return;
149
 
150
  console.log(`🧹 Destroying sandbox: ${sandboxId}`);
 
151
  this.sandboxes.delete(sandboxId);
152
 
153
  try {
 
154
  await fs.rm(sandbox.path, { recursive: true, force: true });
155
  } catch (e) {
156
+ if (e.code !== 'ENOENT') console.error(`Cleanup error for ${sandboxId}:`, e.message);
 
 
 
157
  }
 
158
  }
159
 
160
  async destroyAllSandboxes() {
161
+ await Promise.all(Array.from(this.sandboxes.keys()).map(id => this.destroySandbox(id)));
 
 
162
  }
163
 
164
  getSandbox(sandboxId) {
 
168
  async cleanupInactive() {
169
  const now = Date.now();
170
  for (const [id, sandbox] of this.sandboxes.entries()) {
171
+ if ((now - sandbox.createdAt) > sandbox.timeout) {
172
+ console.log(`⏰ Cleaning up timed-out sandbox: ${id}`);
 
 
 
173
  await this.destroySandbox(id);
174
  }
175
  }