@treesap/sandbox 0.2.0
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 +107 -0
- package/README.md +495 -0
- package/dist/api-server.d.ts +41 -0
- package/dist/api-server.d.ts.map +1 -0
- package/dist/api-server.js +536 -0
- package/dist/api-server.js.map +1 -0
- package/dist/auth-middleware.d.ts +31 -0
- package/dist/auth-middleware.d.ts.map +1 -0
- package/dist/auth-middleware.js +35 -0
- package/dist/auth-middleware.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +65 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +137 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +412 -0
- package/dist/client.js.map +1 -0
- package/dist/file-service.d.ts +94 -0
- package/dist/file-service.d.ts.map +1 -0
- package/dist/file-service.js +203 -0
- package/dist/file-service.js.map +1 -0
- package/dist/http-exposure-service.d.ts +71 -0
- package/dist/http-exposure-service.d.ts.map +1 -0
- package/dist/http-exposure-service.js +172 -0
- package/dist/http-exposure-service.js.map +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox-manager.d.ts +76 -0
- package/dist/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox-manager.js +161 -0
- package/dist/sandbox-manager.js.map +1 -0
- package/dist/sandbox.d.ts +118 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +303 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +240 -0
- package/dist/server.js.map +1 -0
- package/dist/stream-service.d.ts +35 -0
- package/dist/stream-service.d.ts.map +1 -0
- package/dist/stream-service.js +136 -0
- package/dist/stream-service.js.map +1 -0
- package/dist/terminal.d.ts +46 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +264 -0
- package/dist/terminal.js.map +1 -0
- package/dist/websocket.d.ts +48 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +332 -0
- package/dist/websocket.js.map +1 -0
- package/package.json +59 -0
- package/src/api-server.ts +658 -0
- package/src/auth-middleware.ts +65 -0
- package/src/cli.ts +71 -0
- package/src/client.ts +537 -0
- package/src/file-service.ts +273 -0
- package/src/http-exposure-service.ts +232 -0
- package/src/index.ts +101 -0
- package/src/sandbox-manager.ts +202 -0
- package/src/sandbox.ts +396 -0
- package/src/stream-service.ts +174 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Sandbox, SandboxConfig } from './sandbox';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
|
|
6
|
+
export interface SandboxManagerConfig {
|
|
7
|
+
basePath?: string;
|
|
8
|
+
maxSandboxes?: number;
|
|
9
|
+
defaultTimeout?: number;
|
|
10
|
+
autoCleanup?: boolean;
|
|
11
|
+
cleanupInterval?: number;
|
|
12
|
+
maxIdleTime?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Manages multiple sandbox instances
|
|
17
|
+
*/
|
|
18
|
+
export class SandboxManager extends EventEmitter {
|
|
19
|
+
private sandboxes: Map<string, Sandbox> = new Map();
|
|
20
|
+
private lastActivity: Map<string, number> = new Map();
|
|
21
|
+
private config: SandboxManagerConfig;
|
|
22
|
+
private cleanupIntervalId?: NodeJS.Timeout;
|
|
23
|
+
|
|
24
|
+
constructor(config: SandboxManagerConfig = {}) {
|
|
25
|
+
super();
|
|
26
|
+
this.config = {
|
|
27
|
+
basePath: config.basePath || path.join(process.cwd(), '.sandboxes'),
|
|
28
|
+
maxSandboxes: config.maxSandboxes || 100,
|
|
29
|
+
defaultTimeout: config.defaultTimeout || 300000, // 5 minutes
|
|
30
|
+
autoCleanup: config.autoCleanup !== false, // Default true
|
|
31
|
+
cleanupInterval: config.cleanupInterval || 60000, // 1 minute
|
|
32
|
+
maxIdleTime: config.maxIdleTime || 1800000, // 30 minutes
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Ensure basePath is always set
|
|
36
|
+
if (!this.config.basePath) {
|
|
37
|
+
this.config.basePath = path.join(process.cwd(), '.sandboxes');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (this.config.autoCleanup) {
|
|
41
|
+
this.startCleanupTask();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a new sandbox
|
|
47
|
+
*/
|
|
48
|
+
async createSandbox(config: SandboxConfig = {}): Promise<Sandbox> {
|
|
49
|
+
// Check sandbox limit
|
|
50
|
+
if (this.config.maxSandboxes && this.sandboxes.size >= this.config.maxSandboxes) {
|
|
51
|
+
throw new Error(`Maximum sandbox limit reached (${this.config.maxSandboxes})`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Generate ID first if not provided
|
|
55
|
+
const sandboxId = config.id || uuidv4();
|
|
56
|
+
|
|
57
|
+
// Create sandbox with base path
|
|
58
|
+
const sandboxConfig: SandboxConfig = {
|
|
59
|
+
...config,
|
|
60
|
+
id: sandboxId,
|
|
61
|
+
workDir: config.workDir || path.join(this.config.basePath!, sandboxId),
|
|
62
|
+
timeout: config.timeout || this.config.defaultTimeout,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const sandbox = new Sandbox(sandboxConfig);
|
|
66
|
+
await sandbox.initialize();
|
|
67
|
+
|
|
68
|
+
// Track sandbox
|
|
69
|
+
this.sandboxes.set(sandbox.id, sandbox);
|
|
70
|
+
this.lastActivity.set(sandbox.id, Date.now());
|
|
71
|
+
|
|
72
|
+
// Listen for sandbox events to track activity
|
|
73
|
+
sandbox.on('output', () => this.updateActivity(sandbox.id));
|
|
74
|
+
sandbox.on('exec_complete', () => this.updateActivity(sandbox.id));
|
|
75
|
+
sandbox.on('process_started', () => this.updateActivity(sandbox.id));
|
|
76
|
+
|
|
77
|
+
this.emit('sandbox_created', { id: sandbox.id });
|
|
78
|
+
|
|
79
|
+
return sandbox;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get a sandbox by ID
|
|
84
|
+
*/
|
|
85
|
+
getSandbox(id: string): Sandbox | undefined {
|
|
86
|
+
const sandbox = this.sandboxes.get(id);
|
|
87
|
+
if (sandbox) {
|
|
88
|
+
this.updateActivity(id);
|
|
89
|
+
}
|
|
90
|
+
return sandbox;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* List all sandboxes
|
|
95
|
+
*/
|
|
96
|
+
listSandboxes(): Array<{ id: string; status: ReturnType<Sandbox['getStatus']> }> {
|
|
97
|
+
return Array.from(this.sandboxes.entries()).map(([id, sandbox]) => ({
|
|
98
|
+
id,
|
|
99
|
+
status: sandbox.getStatus(),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Destroy a sandbox
|
|
105
|
+
*/
|
|
106
|
+
async destroySandbox(id: string, options: { cleanup?: boolean } = {}): Promise<void> {
|
|
107
|
+
const sandbox = this.sandboxes.get(id);
|
|
108
|
+
if (!sandbox) {
|
|
109
|
+
throw new Error(`Sandbox ${id} not found`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await sandbox.destroy(options);
|
|
113
|
+
this.sandboxes.delete(id);
|
|
114
|
+
this.lastActivity.delete(id);
|
|
115
|
+
|
|
116
|
+
this.emit('sandbox_destroyed', { id });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Destroy all sandboxes
|
|
121
|
+
*/
|
|
122
|
+
async destroyAll(options: { cleanup?: boolean } = {}): Promise<void> {
|
|
123
|
+
const destroyPromises = Array.from(this.sandboxes.keys()).map((id) =>
|
|
124
|
+
this.destroySandbox(id, options).catch((err) => {
|
|
125
|
+
console.error(`Error destroying sandbox ${id}:`, err);
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
await Promise.all(destroyPromises);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get manager statistics
|
|
134
|
+
*/
|
|
135
|
+
getStats() {
|
|
136
|
+
const sandboxList = this.listSandboxes();
|
|
137
|
+
const totalProcesses = sandboxList.reduce(
|
|
138
|
+
(sum, { status }) => sum + status.processCount,
|
|
139
|
+
0
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
totalSandboxes: this.sandboxes.size,
|
|
144
|
+
maxSandboxes: this.config.maxSandboxes,
|
|
145
|
+
totalProcesses,
|
|
146
|
+
basePath: this.config.basePath,
|
|
147
|
+
autoCleanup: this.config.autoCleanup,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Start periodic cleanup task
|
|
153
|
+
*/
|
|
154
|
+
private startCleanupTask(): void {
|
|
155
|
+
this.cleanupIntervalId = setInterval(() => {
|
|
156
|
+
this.cleanupIdleSandboxes();
|
|
157
|
+
}, this.config.cleanupInterval);
|
|
158
|
+
|
|
159
|
+
// Don't keep process alive for cleanup
|
|
160
|
+
this.cleanupIntervalId.unref();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Clean up idle sandboxes
|
|
165
|
+
*/
|
|
166
|
+
private async cleanupIdleSandboxes(): Promise<void> {
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const maxIdleTime = this.config.maxIdleTime!;
|
|
169
|
+
|
|
170
|
+
const idleSandboxes = Array.from(this.lastActivity.entries())
|
|
171
|
+
.filter(([_, lastActive]) => now - lastActive > maxIdleTime)
|
|
172
|
+
.map(([id]) => id);
|
|
173
|
+
|
|
174
|
+
for (const id of idleSandboxes) {
|
|
175
|
+
try {
|
|
176
|
+
await this.destroySandbox(id, { cleanup: true });
|
|
177
|
+
this.emit('sandbox_cleaned_up', { id, reason: 'idle' });
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`Error cleaning up sandbox ${id}:`, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Update last activity time for a sandbox
|
|
186
|
+
*/
|
|
187
|
+
private updateActivity(id: string): void {
|
|
188
|
+
this.lastActivity.set(id, Date.now());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Shutdown the manager (cleanup all sandboxes)
|
|
193
|
+
*/
|
|
194
|
+
async shutdown(options: { cleanup?: boolean } = {}): Promise<void> {
|
|
195
|
+
if (this.cleanupIntervalId) {
|
|
196
|
+
clearInterval(this.cleanupIntervalId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await this.destroyAll(options);
|
|
200
|
+
this.removeAllListeners();
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
|
|
7
|
+
export interface SandboxConfig {
|
|
8
|
+
id?: string;
|
|
9
|
+
workDir?: string;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
maxProcesses?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProcessInfo {
|
|
16
|
+
id: string;
|
|
17
|
+
pid: number;
|
|
18
|
+
command: string;
|
|
19
|
+
status: 'running' | 'completed' | 'failed';
|
|
20
|
+
startTime: number;
|
|
21
|
+
exitCode?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ExecOptions {
|
|
25
|
+
stream?: boolean;
|
|
26
|
+
onOutput?: (stream: 'stdout' | 'stderr', data: string) => void;
|
|
27
|
+
timeout?: number;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
env?: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ExecuteResponse {
|
|
33
|
+
success: boolean;
|
|
34
|
+
stdout: string;
|
|
35
|
+
stderr: string;
|
|
36
|
+
exitCode: number;
|
|
37
|
+
timedOut?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Represents a single isolated sandbox instance
|
|
42
|
+
* Each sandbox has its own working directory and process management
|
|
43
|
+
*/
|
|
44
|
+
export class Sandbox extends EventEmitter {
|
|
45
|
+
public readonly id: string;
|
|
46
|
+
public readonly workDir: string;
|
|
47
|
+
public readonly createdAt: number;
|
|
48
|
+
private processes: Map<string, ProcessInfo> = new Map();
|
|
49
|
+
private runningProcesses: Map<string, ChildProcess> = new Map();
|
|
50
|
+
private config: SandboxConfig;
|
|
51
|
+
private destroyed: boolean = false;
|
|
52
|
+
|
|
53
|
+
constructor(config: SandboxConfig = {}) {
|
|
54
|
+
super();
|
|
55
|
+
this.id = config.id || uuidv4();
|
|
56
|
+
this.workDir = config.workDir || path.join(process.cwd(), '.sandboxes', this.id);
|
|
57
|
+
this.config = { ...config, env: config.env || {} };
|
|
58
|
+
this.createdAt = Date.now();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initialize the sandbox (create working directory)
|
|
63
|
+
*/
|
|
64
|
+
async initialize(): Promise<void> {
|
|
65
|
+
if (this.destroyed) {
|
|
66
|
+
throw new Error('Sandbox has been destroyed');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create working directory
|
|
70
|
+
await fs.mkdir(this.workDir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
this.emit('initialized', { id: this.id, workDir: this.workDir });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Execute a command and return the complete result
|
|
77
|
+
*/
|
|
78
|
+
async exec(command: string, options: ExecOptions = {}): Promise<ExecuteResponse> {
|
|
79
|
+
if (this.destroyed) {
|
|
80
|
+
throw new Error('Sandbox has been destroyed');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
let stdout = '';
|
|
85
|
+
let stderr = '';
|
|
86
|
+
let timedOut = false;
|
|
87
|
+
|
|
88
|
+
// Parse command and args
|
|
89
|
+
const [cmd, ...args] = this.parseCommand(command);
|
|
90
|
+
|
|
91
|
+
const childProcess = spawn(cmd, args, {
|
|
92
|
+
cwd: options.cwd || this.workDir,
|
|
93
|
+
env: { ...process.env, ...this.config.env, ...options.env },
|
|
94
|
+
shell: true,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Handle timeout
|
|
98
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
99
|
+
const timeout = options.timeout || this.config.timeout;
|
|
100
|
+
if (timeout) {
|
|
101
|
+
timeoutId = setTimeout(() => {
|
|
102
|
+
timedOut = true;
|
|
103
|
+
childProcess.kill('SIGTERM');
|
|
104
|
+
|
|
105
|
+
// Force kill after 5 seconds
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
if (!childProcess.killed) {
|
|
108
|
+
childProcess.kill('SIGKILL');
|
|
109
|
+
}
|
|
110
|
+
}, 5000);
|
|
111
|
+
}, timeout);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Capture stdout
|
|
115
|
+
childProcess.stdout?.on('data', (data: Buffer) => {
|
|
116
|
+
const text = data.toString();
|
|
117
|
+
stdout += text;
|
|
118
|
+
|
|
119
|
+
if (options.stream && options.onOutput) {
|
|
120
|
+
options.onOutput('stdout', text);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.emit('output', { stream: 'stdout', data: text });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Capture stderr
|
|
127
|
+
childProcess.stderr?.on('data', (data: Buffer) => {
|
|
128
|
+
const text = data.toString();
|
|
129
|
+
stderr += text;
|
|
130
|
+
|
|
131
|
+
if (options.stream && options.onOutput) {
|
|
132
|
+
options.onOutput('stderr', text);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.emit('output', { stream: 'stderr', data: text });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Handle process completion
|
|
139
|
+
childProcess.on('close', (exitCode) => {
|
|
140
|
+
if (timeoutId) {
|
|
141
|
+
clearTimeout(timeoutId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const response: ExecuteResponse = {
|
|
145
|
+
success: !timedOut && exitCode === 0,
|
|
146
|
+
stdout,
|
|
147
|
+
stderr,
|
|
148
|
+
exitCode: exitCode || 0,
|
|
149
|
+
timedOut,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
this.emit('exec_complete', response);
|
|
153
|
+
resolve(response);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
childProcess.on('error', (error) => {
|
|
157
|
+
if (timeoutId) {
|
|
158
|
+
clearTimeout(timeoutId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const response: ExecuteResponse = {
|
|
162
|
+
success: false,
|
|
163
|
+
stdout,
|
|
164
|
+
stderr: stderr + '\n' + error.message,
|
|
165
|
+
exitCode: 1,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
this.emit('exec_error', error);
|
|
169
|
+
resolve(response);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Start a long-running background process
|
|
176
|
+
*/
|
|
177
|
+
async startProcess(command: string, options: ExecOptions = {}): Promise<ProcessInfo> {
|
|
178
|
+
if (this.destroyed) {
|
|
179
|
+
throw new Error('Sandbox has been destroyed');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check max processes limit
|
|
183
|
+
if (this.config.maxProcesses && this.runningProcesses.size >= this.config.maxProcesses) {
|
|
184
|
+
throw new Error(`Maximum process limit reached (${this.config.maxProcesses})`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const processId = uuidv4();
|
|
188
|
+
const [cmd, ...args] = this.parseCommand(command);
|
|
189
|
+
|
|
190
|
+
const childProcess = spawn(cmd, args, {
|
|
191
|
+
cwd: options.cwd || this.workDir,
|
|
192
|
+
env: { ...process.env, ...this.config.env, ...options.env },
|
|
193
|
+
shell: true,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const processInfo: ProcessInfo = {
|
|
197
|
+
id: processId,
|
|
198
|
+
pid: childProcess.pid!,
|
|
199
|
+
command,
|
|
200
|
+
status: 'running',
|
|
201
|
+
startTime: Date.now(),
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
this.processes.set(processId, processInfo);
|
|
205
|
+
this.runningProcesses.set(processId, childProcess);
|
|
206
|
+
|
|
207
|
+
// Listen for output
|
|
208
|
+
childProcess.stdout?.on('data', (data: Buffer) => {
|
|
209
|
+
this.emit('process_output', {
|
|
210
|
+
processId,
|
|
211
|
+
stream: 'stdout',
|
|
212
|
+
data: data.toString(),
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
childProcess.stderr?.on('data', (data: Buffer) => {
|
|
217
|
+
this.emit('process_output', {
|
|
218
|
+
processId,
|
|
219
|
+
stream: 'stderr',
|
|
220
|
+
data: data.toString(),
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Handle process exit
|
|
225
|
+
childProcess.on('close', (exitCode) => {
|
|
226
|
+
const info = this.processes.get(processId);
|
|
227
|
+
if (info) {
|
|
228
|
+
info.status = exitCode === 0 ? 'completed' : 'failed';
|
|
229
|
+
info.exitCode = exitCode || 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.runningProcesses.delete(processId);
|
|
233
|
+
this.emit('process_exit', { processId, exitCode });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
this.emit('process_started', processInfo);
|
|
237
|
+
return processInfo;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* List all processes (running and completed)
|
|
242
|
+
*/
|
|
243
|
+
listProcesses(): ProcessInfo[] {
|
|
244
|
+
return Array.from(this.processes.values());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get information about a specific process
|
|
249
|
+
*/
|
|
250
|
+
getProcess(processId: string): ProcessInfo | undefined {
|
|
251
|
+
return this.processes.get(processId);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Kill a specific process
|
|
256
|
+
*/
|
|
257
|
+
async killProcess(processId: string, signal: string = 'SIGTERM'): Promise<void> {
|
|
258
|
+
const childProcess = this.runningProcesses.get(processId);
|
|
259
|
+
if (!childProcess) {
|
|
260
|
+
throw new Error(`Process ${processId} not found or already stopped`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
childProcess.kill(signal as NodeJS.Signals);
|
|
264
|
+
|
|
265
|
+
// Update process info
|
|
266
|
+
const info = this.processes.get(processId);
|
|
267
|
+
if (info) {
|
|
268
|
+
info.status = 'failed';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Kill all running processes
|
|
274
|
+
*/
|
|
275
|
+
async killAllProcesses(signal: string = 'SIGTERM'): Promise<void> {
|
|
276
|
+
const killPromises = Array.from(this.runningProcesses.keys()).map((processId) =>
|
|
277
|
+
this.killProcess(processId, signal).catch(() => {
|
|
278
|
+
// Ignore errors if process already stopped
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
await Promise.all(killPromises);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get sandbox status
|
|
287
|
+
*/
|
|
288
|
+
getStatus() {
|
|
289
|
+
return {
|
|
290
|
+
id: this.id,
|
|
291
|
+
workDir: this.workDir,
|
|
292
|
+
createdAt: this.createdAt,
|
|
293
|
+
uptime: Date.now() - this.createdAt,
|
|
294
|
+
processCount: this.runningProcesses.size,
|
|
295
|
+
totalProcesses: this.processes.size,
|
|
296
|
+
destroyed: this.destroyed,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// Environment Variable Management
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Set a single environment variable
|
|
306
|
+
*/
|
|
307
|
+
setEnv(key: string, value: string): void {
|
|
308
|
+
if (this.destroyed) {
|
|
309
|
+
throw new Error('Sandbox has been destroyed');
|
|
310
|
+
}
|
|
311
|
+
this.config.env![key] = value;
|
|
312
|
+
this.emit('env_changed', { key, value, action: 'set' });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Set multiple environment variables at once
|
|
317
|
+
*/
|
|
318
|
+
setEnvBatch(variables: Record<string, string>): void {
|
|
319
|
+
if (this.destroyed) {
|
|
320
|
+
throw new Error('Sandbox has been destroyed');
|
|
321
|
+
}
|
|
322
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
323
|
+
this.config.env![key] = value;
|
|
324
|
+
}
|
|
325
|
+
this.emit('env_changed', { variables, action: 'batch_set' });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get environment variable(s)
|
|
330
|
+
* If key is provided, returns the value for that key
|
|
331
|
+
* Otherwise returns all environment variables
|
|
332
|
+
*/
|
|
333
|
+
getEnv(): Record<string, string>;
|
|
334
|
+
getEnv(key: string): string | undefined;
|
|
335
|
+
getEnv(key?: string): Record<string, string> | string | undefined {
|
|
336
|
+
if (key !== undefined) {
|
|
337
|
+
return this.config.env![key];
|
|
338
|
+
}
|
|
339
|
+
return { ...this.config.env };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get list of environment variable names (for security, not exposing values)
|
|
344
|
+
*/
|
|
345
|
+
getEnvKeys(): string[] {
|
|
346
|
+
return Object.keys(this.config.env!);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Unset (remove) an environment variable
|
|
351
|
+
*/
|
|
352
|
+
unsetEnv(key: string): boolean {
|
|
353
|
+
if (this.destroyed) {
|
|
354
|
+
throw new Error('Sandbox has been destroyed');
|
|
355
|
+
}
|
|
356
|
+
if (key in this.config.env!) {
|
|
357
|
+
delete this.config.env![key];
|
|
358
|
+
this.emit('env_changed', { key, action: 'unset' });
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Destroy the sandbox (kill all processes, optionally clean up files)
|
|
366
|
+
*/
|
|
367
|
+
async destroy(options: { cleanup?: boolean } = {}): Promise<void> {
|
|
368
|
+
if (this.destroyed) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Kill all running processes
|
|
373
|
+
await this.killAllProcesses('SIGKILL');
|
|
374
|
+
|
|
375
|
+
// Clean up files if requested
|
|
376
|
+
if (options.cleanup) {
|
|
377
|
+
try {
|
|
378
|
+
await fs.rm(this.workDir, { recursive: true, force: true });
|
|
379
|
+
} catch (error) {
|
|
380
|
+
// Ignore cleanup errors
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.destroyed = true;
|
|
385
|
+
this.emit('destroyed', { id: this.id });
|
|
386
|
+
this.removeAllListeners();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Parse a command string into command and arguments
|
|
391
|
+
*/
|
|
392
|
+
private parseCommand(command: string): string[] {
|
|
393
|
+
// Simple parsing - for more complex commands, shell: true handles it
|
|
394
|
+
return command.split(' ').filter(Boolean);
|
|
395
|
+
}
|
|
396
|
+
}
|