@cloudflare/sandbox 0.0.8 → 0.1.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 +16 -0
- package/Dockerfile +73 -9
- package/container_src/handler/exec.ts +337 -0
- package/container_src/handler/file.ts +844 -0
- package/container_src/handler/git.ts +182 -0
- package/container_src/handler/ports.ts +314 -0
- package/container_src/handler/process.ts +640 -0
- package/container_src/index.ts +102 -2647
- package/container_src/types.ts +103 -0
- package/dist/chunk-6THNBO4S.js +46 -0
- package/dist/chunk-6THNBO4S.js.map +1 -0
- package/dist/chunk-6UAWTJ5S.js +85 -0
- package/dist/chunk-6UAWTJ5S.js.map +1 -0
- package/dist/chunk-G4XT4SP7.js +638 -0
- package/dist/chunk-G4XT4SP7.js.map +1 -0
- package/dist/chunk-ISFOIYQC.js +585 -0
- package/dist/chunk-ISFOIYQC.js.map +1 -0
- package/dist/chunk-NNGBXDMY.js +89 -0
- package/dist/chunk-NNGBXDMY.js.map +1 -0
- package/dist/client-Da-mLX4p.d.ts +210 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.js +3 -37
- package/dist/index.d.ts +5 -200
- package/dist/index.js +17 -106
- package/dist/index.js.map +1 -1
- package/dist/request-handler.d.ts +16 -0
- package/dist/request-handler.js +12 -0
- package/dist/request-handler.js.map +1 -0
- package/dist/sandbox.d.ts +3 -0
- package/dist/sandbox.js +12 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/security.d.ts +30 -0
- package/dist/security.js +13 -0
- package/dist/security.js.map +1 -0
- package/dist/sse-parser.d.ts +28 -0
- package/dist/sse-parser.js +11 -0
- package/dist/sse-parser.js.map +1 -0
- package/dist/types.d.ts +284 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +2 -7
- package/src/client.ts +320 -1242
- package/src/index.ts +20 -136
- package/src/request-handler.ts +144 -0
- package/src/sandbox.ts +645 -0
- package/src/security.ts +113 -0
- package/src/sse-parser.ts +147 -0
- package/src/types.ts +386 -0
- package/README.md +0 -65
- package/dist/chunk-7WZJ3TRE.js +0 -1364
- package/dist/chunk-7WZJ3TRE.js.map +0 -1
- package/tests/client.example.ts +0 -308
- package/tests/connection-test.ts +0 -81
- package/tests/simple-test.ts +0 -81
- package/tests/test1.ts +0 -281
- package/tests/test2.ts +0 -929
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import { type SpawnOptions, spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import type { ProcessRecord, ProcessStatus, StartProcessRequest } from "../types";
|
|
4
|
+
|
|
5
|
+
// Generate a unique process ID using cryptographically secure randomness
|
|
6
|
+
function generateProcessId(): string {
|
|
7
|
+
return `proc_${Date.now()}_${randomBytes(6).toString('hex')}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
// Process management handlers
|
|
12
|
+
export async function handleStartProcessRequest(
|
|
13
|
+
processes: Map<string, ProcessRecord>,
|
|
14
|
+
req: Request,
|
|
15
|
+
corsHeaders: Record<string, string>
|
|
16
|
+
): Promise<Response> {
|
|
17
|
+
try {
|
|
18
|
+
const body = (await req.json()) as StartProcessRequest;
|
|
19
|
+
const { command, options = {} } = body;
|
|
20
|
+
|
|
21
|
+
if (!command || typeof command !== "string") {
|
|
22
|
+
return new Response(
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
error: "Command is required and must be a string",
|
|
25
|
+
}),
|
|
26
|
+
{
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
...corsHeaders,
|
|
30
|
+
},
|
|
31
|
+
status: 400,
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const processId = options.processId || generateProcessId();
|
|
37
|
+
const startTime = new Date();
|
|
38
|
+
|
|
39
|
+
// Check if process ID already exists
|
|
40
|
+
if (processes.has(processId)) {
|
|
41
|
+
return new Response(
|
|
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
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`[Server] Starting background process: ${command} (ID: ${processId})`);
|
|
56
|
+
|
|
57
|
+
// Create process record in starting state
|
|
58
|
+
const processRecord: ProcessRecord = {
|
|
59
|
+
id: processId,
|
|
60
|
+
command,
|
|
61
|
+
status: 'starting',
|
|
62
|
+
startTime,
|
|
63
|
+
sessionId: options.sessionId,
|
|
64
|
+
stdout: '',
|
|
65
|
+
stderr: '',
|
|
66
|
+
outputListeners: new Set(),
|
|
67
|
+
statusListeners: new Set()
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
processes.set(processId, processRecord);
|
|
71
|
+
|
|
72
|
+
// Start the actual process
|
|
73
|
+
try {
|
|
74
|
+
const spawnOptions: SpawnOptions = {
|
|
75
|
+
cwd: options.cwd || process.cwd(),
|
|
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);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return new Response(
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
process: {
|
|
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;
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error("[Server] Error in handleStartProcessRequest:", error);
|
|
175
|
+
return new Response(
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
error: "Failed to start process",
|
|
178
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
179
|
+
}),
|
|
180
|
+
{
|
|
181
|
+
headers: {
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
...corsHeaders,
|
|
184
|
+
},
|
|
185
|
+
status: 500,
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function handleListProcessesRequest(
|
|
192
|
+
processes: Map<string, ProcessRecord>,
|
|
193
|
+
req: Request,
|
|
194
|
+
corsHeaders: Record<string, string>
|
|
195
|
+
): Promise<Response> {
|
|
196
|
+
try {
|
|
197
|
+
const processesArray = Array.from(processes.values()).map(record => ({
|
|
198
|
+
id: record.id,
|
|
199
|
+
pid: record.pid,
|
|
200
|
+
command: record.command,
|
|
201
|
+
status: record.status,
|
|
202
|
+
startTime: record.startTime.toISOString(),
|
|
203
|
+
endTime: record.endTime?.toISOString(),
|
|
204
|
+
exitCode: record.exitCode,
|
|
205
|
+
sessionId: record.sessionId
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
return new Response(
|
|
209
|
+
JSON.stringify({
|
|
210
|
+
processes: processesArray,
|
|
211
|
+
count: processesArray.length,
|
|
212
|
+
timestamp: new Date().toISOString(),
|
|
213
|
+
}),
|
|
214
|
+
{
|
|
215
|
+
headers: {
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
...corsHeaders,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error("[Server] Error in handleListProcessesRequest:", error);
|
|
223
|
+
return new Response(
|
|
224
|
+
JSON.stringify({
|
|
225
|
+
error: "Failed to list processes",
|
|
226
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
227
|
+
}),
|
|
228
|
+
{
|
|
229
|
+
headers: {
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
...corsHeaders,
|
|
232
|
+
},
|
|
233
|
+
status: 500,
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function handleGetProcessRequest(
|
|
240
|
+
processes: Map<string, ProcessRecord>,
|
|
241
|
+
req: Request,
|
|
242
|
+
corsHeaders: Record<string, string>,
|
|
243
|
+
processId: string
|
|
244
|
+
): Promise<Response> {
|
|
245
|
+
try {
|
|
246
|
+
const record = processes.get(processId);
|
|
247
|
+
|
|
248
|
+
if (!record) {
|
|
249
|
+
return new Response(
|
|
250
|
+
JSON.stringify({
|
|
251
|
+
process: null
|
|
252
|
+
}),
|
|
253
|
+
{
|
|
254
|
+
headers: {
|
|
255
|
+
"Content-Type": "application/json",
|
|
256
|
+
...corsHeaders,
|
|
257
|
+
},
|
|
258
|
+
status: 404,
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return new Response(
|
|
264
|
+
JSON.stringify({
|
|
265
|
+
process: {
|
|
266
|
+
id: record.id,
|
|
267
|
+
pid: record.pid,
|
|
268
|
+
command: record.command,
|
|
269
|
+
status: record.status,
|
|
270
|
+
startTime: record.startTime.toISOString(),
|
|
271
|
+
endTime: record.endTime?.toISOString(),
|
|
272
|
+
exitCode: record.exitCode,
|
|
273
|
+
sessionId: record.sessionId
|
|
274
|
+
}
|
|
275
|
+
}),
|
|
276
|
+
{
|
|
277
|
+
headers: {
|
|
278
|
+
"Content-Type": "application/json",
|
|
279
|
+
...corsHeaders,
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error("[Server] Error in handleGetProcessRequest:", error);
|
|
285
|
+
return new Response(
|
|
286
|
+
JSON.stringify({
|
|
287
|
+
error: "Failed to get process",
|
|
288
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
289
|
+
}),
|
|
290
|
+
{
|
|
291
|
+
headers: {
|
|
292
|
+
"Content-Type": "application/json",
|
|
293
|
+
...corsHeaders,
|
|
294
|
+
},
|
|
295
|
+
status: 500,
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function handleKillProcessRequest(
|
|
302
|
+
processes: Map<string, ProcessRecord>,
|
|
303
|
+
req: Request,
|
|
304
|
+
corsHeaders: Record<string, string>,
|
|
305
|
+
processId: string
|
|
306
|
+
): Promise<Response> {
|
|
307
|
+
try {
|
|
308
|
+
const record = processes.get(processId);
|
|
309
|
+
|
|
310
|
+
if (!record) {
|
|
311
|
+
return new Response(
|
|
312
|
+
JSON.stringify({
|
|
313
|
+
error: `Process not found: ${processId}`,
|
|
314
|
+
}),
|
|
315
|
+
{
|
|
316
|
+
headers: {
|
|
317
|
+
"Content-Type": "application/json",
|
|
318
|
+
...corsHeaders,
|
|
319
|
+
},
|
|
320
|
+
status: 404,
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (record.childProcess && record.status === 'running') {
|
|
326
|
+
record.childProcess.kill('SIGTERM');
|
|
327
|
+
console.log(`[Server] Sent SIGTERM to process ${processId}`);
|
|
328
|
+
|
|
329
|
+
// Give it a moment to terminate gracefully, then force kill
|
|
330
|
+
setTimeout(() => {
|
|
331
|
+
if (record.childProcess && record.status === 'running') {
|
|
332
|
+
record.childProcess.kill('SIGKILL');
|
|
333
|
+
console.log(`[Server] Force killed process ${processId}`);
|
|
334
|
+
}
|
|
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
|
+
}
|
|
360
|
+
);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error("[Server] Error in handleKillProcessRequest:", error);
|
|
363
|
+
return new Response(
|
|
364
|
+
JSON.stringify({
|
|
365
|
+
error: "Failed to kill process",
|
|
366
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
367
|
+
}),
|
|
368
|
+
{
|
|
369
|
+
headers: {
|
|
370
|
+
"Content-Type": "application/json",
|
|
371
|
+
...corsHeaders,
|
|
372
|
+
},
|
|
373
|
+
status: 500,
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function handleKillAllProcessesRequest(
|
|
380
|
+
processes: Map<string, ProcessRecord>,
|
|
381
|
+
req: Request,
|
|
382
|
+
corsHeaders: Record<string, string>
|
|
383
|
+
): Promise<Response> {
|
|
384
|
+
try {
|
|
385
|
+
let killedCount = 0;
|
|
386
|
+
|
|
387
|
+
for (const [processId, record] of processes) {
|
|
388
|
+
if (record.childProcess && record.status === 'running') {
|
|
389
|
+
try {
|
|
390
|
+
record.childProcess.kill('SIGTERM');
|
|
391
|
+
record.status = 'killed';
|
|
392
|
+
record.endTime = new Date();
|
|
393
|
+
record.exitCode = -1;
|
|
394
|
+
|
|
395
|
+
// Notify status listeners
|
|
396
|
+
for (const listener of record.statusListeners) {
|
|
397
|
+
listener('killed');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
killedCount++;
|
|
401
|
+
console.log(`[Server] Killed process ${processId}`);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error(`[Server] Failed to kill process ${processId}:`, error);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return new Response(
|
|
409
|
+
JSON.stringify({
|
|
410
|
+
success: true,
|
|
411
|
+
killedCount,
|
|
412
|
+
message: `Killed ${killedCount} processes`,
|
|
413
|
+
timestamp: new Date().toISOString(),
|
|
414
|
+
}),
|
|
415
|
+
{
|
|
416
|
+
headers: {
|
|
417
|
+
"Content-Type": "application/json",
|
|
418
|
+
...corsHeaders,
|
|
419
|
+
},
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.error("[Server] Error in handleKillAllProcessesRequest:", error);
|
|
424
|
+
return new Response(
|
|
425
|
+
JSON.stringify({
|
|
426
|
+
error: "Failed to kill all processes",
|
|
427
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
428
|
+
}),
|
|
429
|
+
{
|
|
430
|
+
headers: {
|
|
431
|
+
"Content-Type": "application/json",
|
|
432
|
+
...corsHeaders,
|
|
433
|
+
},
|
|
434
|
+
status: 500,
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export async function handleGetProcessLogsRequest(
|
|
441
|
+
processes: Map<string, ProcessRecord>,
|
|
442
|
+
req: Request,
|
|
443
|
+
corsHeaders: Record<string, string>,
|
|
444
|
+
processId: string
|
|
445
|
+
): Promise<Response> {
|
|
446
|
+
try {
|
|
447
|
+
const record = processes.get(processId);
|
|
448
|
+
|
|
449
|
+
if (!record) {
|
|
450
|
+
return new Response(
|
|
451
|
+
JSON.stringify({
|
|
452
|
+
error: `Process not found: ${processId}`,
|
|
453
|
+
}),
|
|
454
|
+
{
|
|
455
|
+
headers: {
|
|
456
|
+
"Content-Type": "application/json",
|
|
457
|
+
...corsHeaders,
|
|
458
|
+
},
|
|
459
|
+
status: 404,
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return new Response(
|
|
465
|
+
JSON.stringify({
|
|
466
|
+
stdout: record.stdout,
|
|
467
|
+
stderr: record.stderr,
|
|
468
|
+
processId: record.id,
|
|
469
|
+
}),
|
|
470
|
+
{
|
|
471
|
+
headers: {
|
|
472
|
+
"Content-Type": "application/json",
|
|
473
|
+
...corsHeaders,
|
|
474
|
+
},
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error("[Server] Error in handleGetProcessLogsRequest:", error);
|
|
479
|
+
return new Response(
|
|
480
|
+
JSON.stringify({
|
|
481
|
+
error: "Failed to get process logs",
|
|
482
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
483
|
+
}),
|
|
484
|
+
{
|
|
485
|
+
headers: {
|
|
486
|
+
"Content-Type": "application/json",
|
|
487
|
+
...corsHeaders,
|
|
488
|
+
},
|
|
489
|
+
status: 500,
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export async function handleStreamProcessLogsRequest(
|
|
496
|
+
processes: Map<string, ProcessRecord>,
|
|
497
|
+
req: Request,
|
|
498
|
+
corsHeaders: Record<string, string>,
|
|
499
|
+
processId: string
|
|
500
|
+
): Promise<Response> {
|
|
501
|
+
try {
|
|
502
|
+
const record = processes.get(processId);
|
|
503
|
+
|
|
504
|
+
if (!record) {
|
|
505
|
+
return new Response(
|
|
506
|
+
JSON.stringify({
|
|
507
|
+
error: `Process not found: ${processId}`,
|
|
508
|
+
}),
|
|
509
|
+
{
|
|
510
|
+
headers: {
|
|
511
|
+
"Content-Type": "application/json",
|
|
512
|
+
...corsHeaders,
|
|
513
|
+
},
|
|
514
|
+
status: 404,
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Create a readable stream for Server-Sent Events
|
|
520
|
+
let isConnected = true;
|
|
521
|
+
|
|
522
|
+
const stream = new ReadableStream({
|
|
523
|
+
start(controller) {
|
|
524
|
+
// Send existing logs first
|
|
525
|
+
if (record.stdout) {
|
|
526
|
+
const event = `data: ${JSON.stringify({
|
|
527
|
+
type: 'stdout',
|
|
528
|
+
timestamp: new Date().toISOString(),
|
|
529
|
+
data: record.stdout,
|
|
530
|
+
processId,
|
|
531
|
+
sessionId: record.sessionId
|
|
532
|
+
})}\n\n`;
|
|
533
|
+
controller.enqueue(new TextEncoder().encode(event));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (record.stderr) {
|
|
537
|
+
const event = `data: ${JSON.stringify({
|
|
538
|
+
type: 'stderr',
|
|
539
|
+
timestamp: new Date().toISOString(),
|
|
540
|
+
data: record.stderr,
|
|
541
|
+
processId,
|
|
542
|
+
sessionId: record.sessionId
|
|
543
|
+
})}\n\n`;
|
|
544
|
+
controller.enqueue(new TextEncoder().encode(event));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Send status
|
|
548
|
+
const statusEvent = `data: ${JSON.stringify({
|
|
549
|
+
type: 'status',
|
|
550
|
+
timestamp: new Date().toISOString(),
|
|
551
|
+
data: `Process status: ${record.status}`,
|
|
552
|
+
processId,
|
|
553
|
+
sessionId: record.sessionId
|
|
554
|
+
})}\n\n`;
|
|
555
|
+
controller.enqueue(new TextEncoder().encode(statusEvent));
|
|
556
|
+
|
|
557
|
+
// Set up real-time streaming for ongoing output
|
|
558
|
+
const outputListener = (stream: 'stdout' | 'stderr', data: string) => {
|
|
559
|
+
if (!isConnected) return;
|
|
560
|
+
|
|
561
|
+
const event = `data: ${JSON.stringify({
|
|
562
|
+
type: stream,
|
|
563
|
+
timestamp: new Date().toISOString(),
|
|
564
|
+
data,
|
|
565
|
+
processId,
|
|
566
|
+
sessionId: record.sessionId
|
|
567
|
+
})}\n\n`;
|
|
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
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const statusListener = (status: ProcessStatus) => {
|
|
578
|
+
if (!isConnected) return;
|
|
579
|
+
|
|
580
|
+
const event = `data: ${JSON.stringify({
|
|
581
|
+
type: 'status',
|
|
582
|
+
timestamp: new Date().toISOString(),
|
|
583
|
+
data: `Process status: ${status}`,
|
|
584
|
+
processId,
|
|
585
|
+
sessionId: record.sessionId
|
|
586
|
+
})}\n\n`;
|
|
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
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// Add listeners
|
|
606
|
+
record.outputListeners.add(outputListener);
|
|
607
|
+
record.statusListeners.add(statusListener);
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
cancel() {
|
|
611
|
+
isConnected = false;
|
|
612
|
+
console.log(`[Server] Log stream cancelled for process ${processId}`);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return new Response(stream, {
|
|
617
|
+
headers: {
|
|
618
|
+
"Content-Type": "text/event-stream",
|
|
619
|
+
"Cache-Control": "no-cache",
|
|
620
|
+
"Connection": "keep-alive",
|
|
621
|
+
...corsHeaders,
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
} catch (error) {
|
|
625
|
+
console.error("[Server] Error in handleStreamProcessLogsRequest:", error);
|
|
626
|
+
return new Response(
|
|
627
|
+
JSON.stringify({
|
|
628
|
+
error: "Failed to stream process logs",
|
|
629
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
630
|
+
}),
|
|
631
|
+
{
|
|
632
|
+
headers: {
|
|
633
|
+
"Content-Type": "application/json",
|
|
634
|
+
...corsHeaders,
|
|
635
|
+
},
|
|
636
|
+
status: 500,
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|