@cloudflare/sandbox 0.2.4 → 0.3.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/CHANGELOG.md +75 -0
- package/Dockerfile +9 -11
- package/README.md +69 -7
- package/container_src/control-process.ts +784 -0
- package/container_src/handler/exec.ts +99 -254
- package/container_src/handler/file.ts +179 -837
- package/container_src/handler/git.ts +28 -80
- package/container_src/handler/process.ts +443 -515
- package/container_src/handler/session.ts +92 -0
- package/container_src/index.ts +68 -130
- package/container_src/isolation.ts +1038 -0
- package/container_src/shell-escape.ts +42 -0
- package/container_src/types.ts +27 -13
- package/dist/{chunk-HHUDRGPY.js → chunk-BEQUGUY4.js} +2 -2
- package/dist/{chunk-CKIGERRS.js → chunk-LFLJGISB.js} +240 -264
- package/dist/chunk-LFLJGISB.js.map +1 -0
- package/dist/{chunk-3CQ6THKA.js → chunk-SMUEY5JR.js} +85 -103
- package/dist/chunk-SMUEY5JR.js.map +1 -0
- package/dist/{client-Ce40ujDF.d.ts → client-Dny_ro_v.d.ts} +41 -25
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -9
- package/dist/interpreter.d.ts +1 -1
- package/dist/jupyter-client.d.ts +1 -1
- package/dist/jupyter-client.js +2 -2
- package/dist/request-handler.d.ts +1 -1
- package/dist/request-handler.js +3 -5
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +3 -5
- package/dist/types.d.ts +10 -21
- package/dist/types.js +35 -9
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +120 -135
- package/src/index.ts +8 -0
- package/src/sandbox.ts +290 -331
- package/src/types.ts +15 -24
- package/dist/chunk-3CQ6THKA.js.map +0 -1
- package/dist/chunk-6EWSYSO7.js +0 -46
- package/dist/chunk-6EWSYSO7.js.map +0 -1
- package/dist/chunk-CKIGERRS.js.map +0 -1
- /package/dist/{chunk-HHUDRGPY.js.map → chunk-BEQUGUY4.js.map} +0 -0
|
@@ -1,615 +1,550 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import type { Session, SessionManager } from "../isolation";
|
|
3
2
|
import type { ProcessRecord, ProcessStatus, StartProcessRequest } from "../types";
|
|
4
3
|
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
// Process management handlers - all processes are tracked per-session
|
|
5
|
+
|
|
6
|
+
// Helper types for process responses
|
|
7
|
+
interface ProcessInfo {
|
|
8
|
+
id: string;
|
|
9
|
+
pid?: number;
|
|
10
|
+
command: string;
|
|
11
|
+
status: ProcessStatus;
|
|
12
|
+
startTime: string;
|
|
13
|
+
endTime?: string | null;
|
|
14
|
+
exitCode?: number | null;
|
|
15
|
+
sessionId: string;
|
|
8
16
|
}
|
|
9
17
|
|
|
18
|
+
// Helper functions to reduce repetition
|
|
19
|
+
function createErrorResponse(
|
|
20
|
+
error: string,
|
|
21
|
+
message?: string,
|
|
22
|
+
status: number = 500,
|
|
23
|
+
corsHeaders: Record<string, string> = {}
|
|
24
|
+
): Response {
|
|
25
|
+
return new Response(
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
error,
|
|
28
|
+
...(message && { message })
|
|
29
|
+
}),
|
|
30
|
+
{
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
...corsHeaders,
|
|
34
|
+
},
|
|
35
|
+
status,
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createSuccessResponse(
|
|
41
|
+
data: Record<string, unknown>,
|
|
42
|
+
corsHeaders: Record<string, string> = {}
|
|
43
|
+
): Response {
|
|
44
|
+
return new Response(
|
|
45
|
+
JSON.stringify(data),
|
|
46
|
+
{
|
|
47
|
+
headers: {
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
...corsHeaders,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function processRecordToInfo(
|
|
56
|
+
record: ProcessRecord,
|
|
57
|
+
sessionId: string
|
|
58
|
+
): ProcessInfo {
|
|
59
|
+
return {
|
|
60
|
+
id: record.id,
|
|
61
|
+
pid: record.pid,
|
|
62
|
+
command: record.command,
|
|
63
|
+
status: record.status,
|
|
64
|
+
startTime: record.startTime.toISOString(),
|
|
65
|
+
endTime: record.endTime ? record.endTime.toISOString() : null,
|
|
66
|
+
exitCode: record.exitCode ?? null,
|
|
67
|
+
sessionId
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function findProcessAcrossSessions(
|
|
72
|
+
processId: string,
|
|
73
|
+
sessionManager: SessionManager
|
|
74
|
+
): Promise<{ process: ProcessRecord; sessionId: string } | null> {
|
|
75
|
+
for (const sessionId of sessionManager.listSessions()) {
|
|
76
|
+
const session = sessionManager.getSession(sessionId);
|
|
77
|
+
if (session) {
|
|
78
|
+
const process = await session.getProcess(processId);
|
|
79
|
+
if (process) {
|
|
80
|
+
return { process, sessionId };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
10
86
|
|
|
11
|
-
// Process management handlers
|
|
12
87
|
export async function handleStartProcessRequest(
|
|
13
|
-
processes: Map<string, ProcessRecord>,
|
|
14
88
|
req: Request,
|
|
15
|
-
corsHeaders: Record<string, string
|
|
89
|
+
corsHeaders: Record<string, string>,
|
|
90
|
+
sessionManager?: SessionManager
|
|
16
91
|
): Promise<Response> {
|
|
17
92
|
try {
|
|
18
93
|
const body = (await req.json()) as StartProcessRequest;
|
|
19
|
-
const { command, options = {} } = body;
|
|
94
|
+
const { command, sessionId, options = {} } = body;
|
|
20
95
|
|
|
21
96
|
if (!command || typeof command !== "string") {
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
headers: {
|
|
28
|
-
"Content-Type": "application/json",
|
|
29
|
-
...corsHeaders,
|
|
30
|
-
},
|
|
31
|
-
status: 400,
|
|
32
|
-
}
|
|
97
|
+
return createErrorResponse(
|
|
98
|
+
"Command is required and must be a string",
|
|
99
|
+
undefined,
|
|
100
|
+
400,
|
|
101
|
+
corsHeaders
|
|
33
102
|
);
|
|
34
103
|
}
|
|
35
104
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
JSON.stringify({
|
|
43
|
-
error: `Process already exists: ${processId}`,
|
|
44
|
-
}),
|
|
45
|
-
{
|
|
46
|
-
headers: {
|
|
47
|
-
"Content-Type": "application/json",
|
|
48
|
-
...corsHeaders,
|
|
49
|
-
},
|
|
50
|
-
status: 409,
|
|
51
|
-
}
|
|
105
|
+
if (!sessionManager) {
|
|
106
|
+
return createErrorResponse(
|
|
107
|
+
"Session manager is required for process management",
|
|
108
|
+
undefined,
|
|
109
|
+
500,
|
|
110
|
+
corsHeaders
|
|
52
111
|
);
|
|
53
112
|
}
|
|
54
113
|
|
|
55
|
-
console.log(`[Server] Starting
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
processes.set(processId, processRecord);
|
|
71
|
-
|
|
72
|
-
// Start the actual process
|
|
73
|
-
try {
|
|
74
|
-
const spawnOptions: SpawnOptions = {
|
|
75
|
-
cwd: options.cwd || "/workspace", // Default to /workspace for consistency with exec commands
|
|
76
|
-
env: { ...process.env, ...options.env },
|
|
77
|
-
detached: false,
|
|
78
|
-
shell: true,
|
|
79
|
-
stdio: ["pipe", "pipe", "pipe"] as const
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Use shell execution to preserve quotes and complex command structures
|
|
83
|
-
const childProcess = spawn(command, spawnOptions);
|
|
84
|
-
processRecord.childProcess = childProcess;
|
|
85
|
-
processRecord.pid = childProcess.pid;
|
|
86
|
-
processRecord.status = 'running';
|
|
87
|
-
|
|
88
|
-
// Set up output handling
|
|
89
|
-
childProcess.stdout?.on('data', (data) => {
|
|
90
|
-
const output = data.toString(options.encoding || 'utf8');
|
|
91
|
-
processRecord.stdout += output;
|
|
92
|
-
|
|
93
|
-
// Notify listeners
|
|
94
|
-
for (const listener of processRecord.outputListeners) {
|
|
95
|
-
listener('stdout', output);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
childProcess.stderr?.on('data', (data) => {
|
|
100
|
-
const output = data.toString(options.encoding || 'utf8');
|
|
101
|
-
processRecord.stderr += output;
|
|
102
|
-
|
|
103
|
-
// Notify listeners
|
|
104
|
-
for (const listener of processRecord.outputListeners) {
|
|
105
|
-
listener('stderr', output);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
childProcess.on('exit', (code, signal) => {
|
|
110
|
-
processRecord.endTime = new Date();
|
|
111
|
-
processRecord.exitCode = code !== null ? code : -1;
|
|
112
|
-
|
|
113
|
-
if (signal) {
|
|
114
|
-
processRecord.status = 'killed';
|
|
115
|
-
} else if (code === 0) {
|
|
116
|
-
processRecord.status = 'completed';
|
|
117
|
-
} else {
|
|
118
|
-
processRecord.status = 'failed';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Notify status listeners
|
|
122
|
-
for (const listener of processRecord.statusListeners) {
|
|
123
|
-
listener(processRecord.status);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
console.log(`[Server] Process ${processId} exited with code ${code} (signal: ${signal})`);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
childProcess.on('error', (error) => {
|
|
130
|
-
processRecord.status = 'error';
|
|
131
|
-
processRecord.endTime = new Date();
|
|
132
|
-
console.error(`[Server] Process ${processId} error:`, error);
|
|
133
|
-
|
|
134
|
-
// Notify status listeners
|
|
135
|
-
for (const listener of processRecord.statusListeners) {
|
|
136
|
-
listener('error');
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Timeout handling
|
|
141
|
-
if (options.timeout) {
|
|
142
|
-
setTimeout(() => {
|
|
143
|
-
if (processRecord.status === 'running') {
|
|
144
|
-
childProcess.kill('SIGTERM');
|
|
145
|
-
console.log(`[Server] Process ${processId} timed out after ${options.timeout}ms`);
|
|
146
|
-
}
|
|
147
|
-
}, options.timeout);
|
|
114
|
+
console.log(`[Server] Starting process: ${command}${sessionId ? ` in session: ${sessionId}` : ' (default session)'}`);
|
|
115
|
+
|
|
116
|
+
// Get the session (use default if not specified)
|
|
117
|
+
let session: Session;
|
|
118
|
+
|
|
119
|
+
if (sessionId) {
|
|
120
|
+
const specificSession = sessionManager.getSession(sessionId);
|
|
121
|
+
if (!specificSession) {
|
|
122
|
+
return createErrorResponse(
|
|
123
|
+
`Session '${sessionId}' not found`,
|
|
124
|
+
undefined,
|
|
125
|
+
404,
|
|
126
|
+
corsHeaders
|
|
127
|
+
);
|
|
148
128
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
id: processRecord.id,
|
|
154
|
-
pid: processRecord.pid,
|
|
155
|
-
command: processRecord.command,
|
|
156
|
-
status: processRecord.status,
|
|
157
|
-
startTime: processRecord.startTime.toISOString(),
|
|
158
|
-
sessionId: processRecord.sessionId
|
|
159
|
-
}
|
|
160
|
-
}),
|
|
161
|
-
{
|
|
162
|
-
headers: {
|
|
163
|
-
"Content-Type": "application/json",
|
|
164
|
-
...corsHeaders,
|
|
165
|
-
},
|
|
166
|
-
}
|
|
167
|
-
);
|
|
168
|
-
} catch (error) {
|
|
169
|
-
// Clean up on error
|
|
170
|
-
processes.delete(processId);
|
|
171
|
-
throw error;
|
|
129
|
+
session = specificSession;
|
|
130
|
+
} else {
|
|
131
|
+
// Use the centralized method to get or create default session
|
|
132
|
+
session = await sessionManager.getOrCreateDefaultSession();
|
|
172
133
|
}
|
|
134
|
+
|
|
135
|
+
const processRecord = await session.startProcess(command, options);
|
|
136
|
+
|
|
137
|
+
return createSuccessResponse({
|
|
138
|
+
process: processRecordToInfo(processRecord, sessionId || 'default')
|
|
139
|
+
}, corsHeaders);
|
|
173
140
|
} catch (error) {
|
|
174
|
-
console.error("[Server] Error
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{
|
|
181
|
-
headers: {
|
|
182
|
-
"Content-Type": "application/json",
|
|
183
|
-
...corsHeaders,
|
|
184
|
-
},
|
|
185
|
-
status: 500,
|
|
186
|
-
}
|
|
141
|
+
console.error("[Server] Error starting process:", error);
|
|
142
|
+
return createErrorResponse(
|
|
143
|
+
"Failed to start process",
|
|
144
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
145
|
+
500,
|
|
146
|
+
corsHeaders
|
|
187
147
|
);
|
|
188
148
|
}
|
|
189
149
|
}
|
|
190
150
|
|
|
191
151
|
export async function handleListProcessesRequest(
|
|
192
|
-
processes: Map<string, ProcessRecord>,
|
|
193
152
|
req: Request,
|
|
194
|
-
corsHeaders: Record<string, string
|
|
153
|
+
corsHeaders: Record<string, string>,
|
|
154
|
+
sessionManager?: SessionManager
|
|
195
155
|
): Promise<Response> {
|
|
196
156
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
157
|
+
if (!sessionManager) {
|
|
158
|
+
return createErrorResponse(
|
|
159
|
+
"Session manager is required",
|
|
160
|
+
undefined,
|
|
161
|
+
500,
|
|
162
|
+
corsHeaders
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Get the session name from query params if provided
|
|
167
|
+
const url = new URL(req.url);
|
|
168
|
+
const sessionId = url.searchParams.get('session');
|
|
169
|
+
|
|
170
|
+
let allProcesses: ProcessInfo[] = [];
|
|
171
|
+
|
|
172
|
+
if (sessionId) {
|
|
173
|
+
// List processes from specific session
|
|
174
|
+
const session = sessionManager.getSession(sessionId);
|
|
175
|
+
if (!session) {
|
|
176
|
+
return createErrorResponse(
|
|
177
|
+
`Session '${sessionId}' not found`,
|
|
178
|
+
undefined,
|
|
179
|
+
404,
|
|
180
|
+
corsHeaders
|
|
181
|
+
);
|
|
219
182
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
"Content-Type": "application/json",
|
|
231
|
-
...corsHeaders,
|
|
232
|
-
},
|
|
233
|
-
status: 500,
|
|
183
|
+
const processes = await session.listProcesses();
|
|
184
|
+
allProcesses = processes.map(p => processRecordToInfo(p, sessionId));
|
|
185
|
+
} else {
|
|
186
|
+
// List processes from all sessions
|
|
187
|
+
for (const name of sessionManager.listSessions()) {
|
|
188
|
+
const session = sessionManager.getSession(name);
|
|
189
|
+
if (session) {
|
|
190
|
+
const processes = await session.listProcesses();
|
|
191
|
+
allProcesses.push(...processes.map(p => processRecordToInfo(p, name)));
|
|
192
|
+
}
|
|
234
193
|
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return createSuccessResponse({
|
|
197
|
+
processes: allProcesses,
|
|
198
|
+
count: allProcesses.length,
|
|
199
|
+
timestamp: new Date().toISOString(),
|
|
200
|
+
}, corsHeaders);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error("[Server] Error listing processes:", error);
|
|
203
|
+
return createErrorResponse(
|
|
204
|
+
"Failed to list processes",
|
|
205
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
206
|
+
500,
|
|
207
|
+
corsHeaders
|
|
235
208
|
);
|
|
236
209
|
}
|
|
237
210
|
}
|
|
238
211
|
|
|
239
212
|
export async function handleGetProcessRequest(
|
|
240
|
-
processes: Map<string, ProcessRecord>,
|
|
241
213
|
req: Request,
|
|
242
214
|
corsHeaders: Record<string, string>,
|
|
243
|
-
processId: string
|
|
215
|
+
processId: string,
|
|
216
|
+
sessionManager?: SessionManager
|
|
244
217
|
): Promise<Response> {
|
|
245
218
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}),
|
|
253
|
-
{
|
|
254
|
-
headers: {
|
|
255
|
-
"Content-Type": "application/json",
|
|
256
|
-
...corsHeaders,
|
|
257
|
-
},
|
|
258
|
-
status: 404,
|
|
259
|
-
}
|
|
219
|
+
if (!sessionManager) {
|
|
220
|
+
return createErrorResponse(
|
|
221
|
+
"Session manager is required",
|
|
222
|
+
undefined,
|
|
223
|
+
500,
|
|
224
|
+
corsHeaders
|
|
260
225
|
);
|
|
261
226
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
headers: {
|
|
278
|
-
"Content-Type": "application/json",
|
|
279
|
-
...corsHeaders,
|
|
280
|
-
},
|
|
281
|
-
}
|
|
282
|
-
);
|
|
227
|
+
|
|
228
|
+
const result = await findProcessAcrossSessions(processId, sessionManager);
|
|
229
|
+
if (!result) {
|
|
230
|
+
return createErrorResponse(
|
|
231
|
+
"Process not found",
|
|
232
|
+
processId,
|
|
233
|
+
404,
|
|
234
|
+
corsHeaders
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return createSuccessResponse({
|
|
239
|
+
process: processRecordToInfo(result.process, result.sessionId),
|
|
240
|
+
timestamp: new Date().toISOString(),
|
|
241
|
+
}, corsHeaders);
|
|
283
242
|
} catch (error) {
|
|
284
|
-
console.error("[Server] Error
|
|
285
|
-
return
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
{
|
|
291
|
-
headers: {
|
|
292
|
-
"Content-Type": "application/json",
|
|
293
|
-
...corsHeaders,
|
|
294
|
-
},
|
|
295
|
-
status: 500,
|
|
296
|
-
}
|
|
243
|
+
console.error("[Server] Error getting process:", error);
|
|
244
|
+
return createErrorResponse(
|
|
245
|
+
"Failed to get process",
|
|
246
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
247
|
+
500,
|
|
248
|
+
corsHeaders
|
|
297
249
|
);
|
|
298
250
|
}
|
|
299
251
|
}
|
|
300
252
|
|
|
301
253
|
export async function handleKillProcessRequest(
|
|
302
|
-
processes: Map<string, ProcessRecord>,
|
|
303
254
|
req: Request,
|
|
304
255
|
corsHeaders: Record<string, string>,
|
|
305
|
-
processId: string
|
|
256
|
+
processId: string,
|
|
257
|
+
sessionManager?: SessionManager
|
|
306
258
|
): Promise<Response> {
|
|
307
259
|
try {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}),
|
|
315
|
-
{
|
|
316
|
-
headers: {
|
|
317
|
-
"Content-Type": "application/json",
|
|
318
|
-
...corsHeaders,
|
|
319
|
-
},
|
|
320
|
-
status: 404,
|
|
321
|
-
}
|
|
260
|
+
if (!sessionManager) {
|
|
261
|
+
return createErrorResponse(
|
|
262
|
+
"Session manager is required",
|
|
263
|
+
undefined,
|
|
264
|
+
500,
|
|
265
|
+
corsHeaders
|
|
322
266
|
);
|
|
323
267
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
268
|
+
|
|
269
|
+
// Search for and kill the process across all sessions
|
|
270
|
+
for (const sessionId of sessionManager.listSessions()) {
|
|
271
|
+
const session = sessionManager.getSession(sessionId);
|
|
272
|
+
if (session) {
|
|
273
|
+
const process = await session.getProcess(processId);
|
|
274
|
+
if (process) {
|
|
275
|
+
const killed = await session.killProcess(processId);
|
|
276
|
+
return createSuccessResponse({
|
|
277
|
+
success: killed,
|
|
278
|
+
processId,
|
|
279
|
+
sessionId,
|
|
280
|
+
message: killed ? `Process ${processId} killed` : `Failed to kill process ${processId}`,
|
|
281
|
+
timestamp: new Date().toISOString(),
|
|
282
|
+
}, corsHeaders);
|
|
334
283
|
}
|
|
335
|
-
}, 5000);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Mark as killed locally
|
|
339
|
-
record.status = 'killed';
|
|
340
|
-
record.endTime = new Date();
|
|
341
|
-
record.exitCode = -1;
|
|
342
|
-
|
|
343
|
-
// Notify status listeners
|
|
344
|
-
for (const listener of record.statusListeners) {
|
|
345
|
-
listener('killed');
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return new Response(
|
|
349
|
-
JSON.stringify({
|
|
350
|
-
success: true,
|
|
351
|
-
message: `Process ${processId} killed`,
|
|
352
|
-
timestamp: new Date().toISOString(),
|
|
353
|
-
}),
|
|
354
|
-
{
|
|
355
|
-
headers: {
|
|
356
|
-
"Content-Type": "application/json",
|
|
357
|
-
...corsHeaders,
|
|
358
|
-
},
|
|
359
284
|
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return createErrorResponse(
|
|
288
|
+
"Process not found",
|
|
289
|
+
processId,
|
|
290
|
+
404,
|
|
291
|
+
corsHeaders
|
|
360
292
|
);
|
|
361
293
|
} catch (error) {
|
|
362
|
-
console.error("[Server] Error
|
|
363
|
-
return
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
{
|
|
369
|
-
headers: {
|
|
370
|
-
"Content-Type": "application/json",
|
|
371
|
-
...corsHeaders,
|
|
372
|
-
},
|
|
373
|
-
status: 500,
|
|
374
|
-
}
|
|
294
|
+
console.error("[Server] Error killing process:", error);
|
|
295
|
+
return createErrorResponse(
|
|
296
|
+
"Failed to kill process",
|
|
297
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
298
|
+
500,
|
|
299
|
+
corsHeaders
|
|
375
300
|
);
|
|
376
301
|
}
|
|
377
302
|
}
|
|
378
303
|
|
|
379
304
|
export async function handleKillAllProcessesRequest(
|
|
380
|
-
processes: Map<string, ProcessRecord>,
|
|
381
305
|
req: Request,
|
|
382
|
-
corsHeaders: Record<string, string
|
|
306
|
+
corsHeaders: Record<string, string>,
|
|
307
|
+
sessionManager?: SessionManager
|
|
383
308
|
): Promise<Response> {
|
|
384
309
|
try {
|
|
310
|
+
if (!sessionManager) {
|
|
311
|
+
return createErrorResponse(
|
|
312
|
+
"Session manager is required",
|
|
313
|
+
undefined,
|
|
314
|
+
500,
|
|
315
|
+
corsHeaders
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Get the session name from query params if provided
|
|
320
|
+
const url = new URL(req.url);
|
|
321
|
+
const sessionId = url.searchParams.get('session');
|
|
322
|
+
|
|
385
323
|
let killedCount = 0;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
324
|
+
|
|
325
|
+
if (sessionId) {
|
|
326
|
+
// Kill processes in specific session
|
|
327
|
+
const session = sessionManager.getSession(sessionId);
|
|
328
|
+
if (!session) {
|
|
329
|
+
return createErrorResponse(
|
|
330
|
+
`Session '${sessionId}' not found`,
|
|
331
|
+
undefined,
|
|
332
|
+
404,
|
|
333
|
+
corsHeaders
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
killedCount = await session.killAllProcesses();
|
|
337
|
+
} else {
|
|
338
|
+
// Kill processes in all sessions
|
|
339
|
+
for (const name of sessionManager.listSessions()) {
|
|
340
|
+
const session = sessionManager.getSession(name);
|
|
341
|
+
if (session) {
|
|
342
|
+
killedCount += await session.killAllProcesses();
|
|
404
343
|
}
|
|
405
344
|
}
|
|
406
345
|
}
|
|
407
346
|
|
|
408
|
-
return
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}),
|
|
415
|
-
{
|
|
416
|
-
headers: {
|
|
417
|
-
"Content-Type": "application/json",
|
|
418
|
-
...corsHeaders,
|
|
419
|
-
},
|
|
420
|
-
}
|
|
421
|
-
);
|
|
347
|
+
return createSuccessResponse({
|
|
348
|
+
success: true,
|
|
349
|
+
killedCount,
|
|
350
|
+
message: `Killed ${killedCount} process${killedCount !== 1 ? 'es' : ''}`,
|
|
351
|
+
timestamp: new Date().toISOString(),
|
|
352
|
+
}, corsHeaders);
|
|
422
353
|
} catch (error) {
|
|
423
|
-
console.error("[Server] Error
|
|
424
|
-
return
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
{
|
|
430
|
-
headers: {
|
|
431
|
-
"Content-Type": "application/json",
|
|
432
|
-
...corsHeaders,
|
|
433
|
-
},
|
|
434
|
-
status: 500,
|
|
435
|
-
}
|
|
354
|
+
console.error("[Server] Error killing all processes:", error);
|
|
355
|
+
return createErrorResponse(
|
|
356
|
+
"Failed to kill all processes",
|
|
357
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
358
|
+
500,
|
|
359
|
+
corsHeaders
|
|
436
360
|
);
|
|
437
361
|
}
|
|
438
362
|
}
|
|
439
363
|
|
|
440
364
|
export async function handleGetProcessLogsRequest(
|
|
441
|
-
processes: Map<string, ProcessRecord>,
|
|
442
365
|
req: Request,
|
|
443
366
|
corsHeaders: Record<string, string>,
|
|
444
|
-
processId: string
|
|
367
|
+
processId: string,
|
|
368
|
+
sessionManager?: SessionManager
|
|
445
369
|
): Promise<Response> {
|
|
446
370
|
try {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}),
|
|
454
|
-
{
|
|
455
|
-
headers: {
|
|
456
|
-
"Content-Type": "application/json",
|
|
457
|
-
...corsHeaders,
|
|
458
|
-
},
|
|
459
|
-
status: 404,
|
|
460
|
-
}
|
|
371
|
+
if (!sessionManager) {
|
|
372
|
+
return createErrorResponse(
|
|
373
|
+
"Session manager is required",
|
|
374
|
+
undefined,
|
|
375
|
+
500,
|
|
376
|
+
corsHeaders
|
|
461
377
|
);
|
|
462
378
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
processId
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
)
|
|
379
|
+
|
|
380
|
+
const result = await findProcessAcrossSessions(processId, sessionManager);
|
|
381
|
+
if (!result) {
|
|
382
|
+
return createErrorResponse(
|
|
383
|
+
"Process not found",
|
|
384
|
+
processId,
|
|
385
|
+
404,
|
|
386
|
+
corsHeaders
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Get the session and use its getProcessLogs method to ensure logs are updated from files
|
|
391
|
+
const session = sessionManager.getSession(result.sessionId);
|
|
392
|
+
if (!session) {
|
|
393
|
+
return createErrorResponse(
|
|
394
|
+
"Session not found",
|
|
395
|
+
result.sessionId,
|
|
396
|
+
500,
|
|
397
|
+
corsHeaders
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// This will update logs from temp files before returning
|
|
402
|
+
const logs = await session.getProcessLogs(processId);
|
|
403
|
+
|
|
404
|
+
return createSuccessResponse({
|
|
405
|
+
stdout: logs.stdout,
|
|
406
|
+
stderr: logs.stderr,
|
|
407
|
+
processId,
|
|
408
|
+
sessionId: result.sessionId,
|
|
409
|
+
timestamp: new Date().toISOString(),
|
|
410
|
+
}, corsHeaders);
|
|
477
411
|
} catch (error) {
|
|
478
|
-
console.error("[Server] Error
|
|
479
|
-
return
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
{
|
|
485
|
-
headers: {
|
|
486
|
-
"Content-Type": "application/json",
|
|
487
|
-
...corsHeaders,
|
|
488
|
-
},
|
|
489
|
-
status: 500,
|
|
490
|
-
}
|
|
412
|
+
console.error("[Server] Error getting process logs:", error);
|
|
413
|
+
return createErrorResponse(
|
|
414
|
+
"Failed to get process logs",
|
|
415
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
416
|
+
500,
|
|
417
|
+
corsHeaders
|
|
491
418
|
);
|
|
492
419
|
}
|
|
493
420
|
}
|
|
494
421
|
|
|
495
422
|
export async function handleStreamProcessLogsRequest(
|
|
496
|
-
processes: Map<string, ProcessRecord>,
|
|
497
423
|
req: Request,
|
|
498
424
|
corsHeaders: Record<string, string>,
|
|
499
|
-
processId: string
|
|
425
|
+
processId: string,
|
|
426
|
+
sessionManager?: SessionManager
|
|
500
427
|
): Promise<Response> {
|
|
501
428
|
try {
|
|
502
|
-
|
|
429
|
+
if (!sessionManager) {
|
|
430
|
+
return createErrorResponse(
|
|
431
|
+
"Session manager is required",
|
|
432
|
+
undefined,
|
|
433
|
+
500,
|
|
434
|
+
corsHeaders
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const result = await findProcessAcrossSessions(processId, sessionManager);
|
|
439
|
+
if (!result) {
|
|
440
|
+
return createErrorResponse(
|
|
441
|
+
"Process not found",
|
|
442
|
+
processId,
|
|
443
|
+
404,
|
|
444
|
+
corsHeaders
|
|
445
|
+
);
|
|
446
|
+
}
|
|
503
447
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
status: 404,
|
|
515
|
-
}
|
|
448
|
+
const { process: targetProcess, sessionId } = result;
|
|
449
|
+
|
|
450
|
+
// Get the session to start monitoring
|
|
451
|
+
const session = sessionManager.getSession(sessionId);
|
|
452
|
+
if (!session) {
|
|
453
|
+
return createErrorResponse(
|
|
454
|
+
"Session not found",
|
|
455
|
+
sessionId,
|
|
456
|
+
404,
|
|
457
|
+
corsHeaders
|
|
516
458
|
);
|
|
517
459
|
}
|
|
518
460
|
|
|
519
|
-
//
|
|
520
|
-
let
|
|
461
|
+
// Store listeners outside the stream for proper cleanup
|
|
462
|
+
let outputListener: ((stream: 'stdout' | 'stderr', data: string) => void) | null = null;
|
|
463
|
+
let statusListener: ((status: ProcessStatus) => void) | null = null;
|
|
521
464
|
|
|
465
|
+
// Create a stream that sends updates
|
|
522
466
|
const stream = new ReadableStream({
|
|
523
467
|
start(controller) {
|
|
524
|
-
// Send
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
type: 'stdout',
|
|
528
|
-
|
|
529
|
-
data: record.stdout,
|
|
468
|
+
// Send initial logs
|
|
469
|
+
if (targetProcess.stdout) {
|
|
470
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({
|
|
471
|
+
type: 'stdout',
|
|
472
|
+
data: targetProcess.stdout,
|
|
530
473
|
processId,
|
|
531
|
-
sessionId
|
|
532
|
-
|
|
533
|
-
|
|
474
|
+
sessionId,
|
|
475
|
+
timestamp: new Date().toISOString()
|
|
476
|
+
})}\n\n`));
|
|
534
477
|
}
|
|
535
|
-
|
|
536
|
-
if (
|
|
537
|
-
|
|
538
|
-
type: 'stderr',
|
|
539
|
-
|
|
540
|
-
data: record.stderr,
|
|
478
|
+
|
|
479
|
+
if (targetProcess.stderr) {
|
|
480
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({
|
|
481
|
+
type: 'stderr',
|
|
482
|
+
data: targetProcess.stderr,
|
|
541
483
|
processId,
|
|
542
|
-
sessionId
|
|
543
|
-
|
|
544
|
-
|
|
484
|
+
sessionId,
|
|
485
|
+
timestamp: new Date().toISOString()
|
|
486
|
+
})}\n\n`));
|
|
545
487
|
}
|
|
546
|
-
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
488
|
+
|
|
489
|
+
// If process is complete, send completion and close
|
|
490
|
+
if (targetProcess.status === 'completed' || targetProcess.status === 'failed' || targetProcess.status === 'killed') {
|
|
491
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({
|
|
492
|
+
type: 'complete',
|
|
493
|
+
status: targetProcess.status,
|
|
494
|
+
exitCode: targetProcess.exitCode,
|
|
495
|
+
processId,
|
|
496
|
+
sessionId,
|
|
497
|
+
timestamp: new Date().toISOString()
|
|
498
|
+
})}\n\n`));
|
|
499
|
+
controller.close();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Set up listeners for live updates
|
|
504
|
+
outputListener = (stream: 'stdout' | 'stderr', data: string) => {
|
|
505
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({
|
|
506
|
+
type: stream,
|
|
564
507
|
data,
|
|
565
508
|
processId,
|
|
566
|
-
sessionId
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
try {
|
|
570
|
-
controller.enqueue(new TextEncoder().encode(event));
|
|
571
|
-
} catch (error) {
|
|
572
|
-
console.log(`[Server] Stream closed for process ${processId}`);
|
|
573
|
-
isConnected = false;
|
|
574
|
-
}
|
|
509
|
+
sessionId,
|
|
510
|
+
timestamp: new Date().toISOString()
|
|
511
|
+
})}\n\n`));
|
|
575
512
|
};
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
try {
|
|
589
|
-
controller.enqueue(new TextEncoder().encode(event));
|
|
590
|
-
} catch (error) {
|
|
591
|
-
console.log(`[Server] Stream closed for process ${processId}`);
|
|
592
|
-
isConnected = false;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Close stream when process completes
|
|
596
|
-
if (['completed', 'failed', 'killed', 'error'].includes(status)) {
|
|
597
|
-
setTimeout(() => {
|
|
598
|
-
record.outputListeners.delete(outputListener);
|
|
599
|
-
record.statusListeners.delete(statusListener);
|
|
600
|
-
controller.close();
|
|
601
|
-
}, 1000); // Give a moment for final events
|
|
513
|
+
|
|
514
|
+
statusListener = (status: ProcessStatus) => {
|
|
515
|
+
if (status === 'completed' || status === 'failed' || status === 'killed') {
|
|
516
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({
|
|
517
|
+
type: 'complete',
|
|
518
|
+
status,
|
|
519
|
+
exitCode: targetProcess.exitCode,
|
|
520
|
+
processId,
|
|
521
|
+
sessionId,
|
|
522
|
+
timestamp: new Date().toISOString()
|
|
523
|
+
})}\n\n`));
|
|
524
|
+
controller.close();
|
|
602
525
|
}
|
|
603
526
|
};
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
527
|
+
|
|
528
|
+
targetProcess.outputListeners.add(outputListener);
|
|
529
|
+
targetProcess.statusListeners.add(statusListener);
|
|
530
|
+
|
|
531
|
+
// Start monitoring the process for output changes
|
|
532
|
+
session.startProcessMonitoring(targetProcess);
|
|
608
533
|
},
|
|
609
|
-
|
|
610
534
|
cancel() {
|
|
611
|
-
|
|
612
|
-
|
|
535
|
+
// Clean up when stream is closed (client disconnects)
|
|
536
|
+
// Remove only this stream's listeners, not all listeners
|
|
537
|
+
if (outputListener) {
|
|
538
|
+
targetProcess.outputListeners.delete(outputListener);
|
|
539
|
+
}
|
|
540
|
+
if (statusListener) {
|
|
541
|
+
targetProcess.statusListeners.delete(statusListener);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Stop monitoring if no more listeners
|
|
545
|
+
if (targetProcess.outputListeners.size === 0) {
|
|
546
|
+
session.stopProcessMonitoring(targetProcess);
|
|
547
|
+
}
|
|
613
548
|
}
|
|
614
549
|
});
|
|
615
550
|
|
|
@@ -622,19 +557,12 @@ export async function handleStreamProcessLogsRequest(
|
|
|
622
557
|
},
|
|
623
558
|
});
|
|
624
559
|
} catch (error) {
|
|
625
|
-
console.error("[Server] Error
|
|
626
|
-
return
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
{
|
|
632
|
-
headers: {
|
|
633
|
-
"Content-Type": "application/json",
|
|
634
|
-
...corsHeaders,
|
|
635
|
-
},
|
|
636
|
-
status: 500,
|
|
637
|
-
}
|
|
560
|
+
console.error("[Server] Error streaming process logs:", error);
|
|
561
|
+
return createErrorResponse(
|
|
562
|
+
"Failed to stream process logs",
|
|
563
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
564
|
+
500,
|
|
565
|
+
corsHeaders
|
|
638
566
|
);
|
|
639
567
|
}
|
|
640
|
-
}
|
|
568
|
+
}
|