@cloudflare/sandbox 0.2.3 → 0.3.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/Dockerfile +9 -11
  3. package/README.md +69 -7
  4. package/container_src/control-process.ts +784 -0
  5. package/container_src/handler/exec.ts +99 -254
  6. package/container_src/handler/file.ts +204 -642
  7. package/container_src/handler/git.ts +28 -80
  8. package/container_src/handler/process.ts +443 -515
  9. package/container_src/handler/session.ts +92 -0
  10. package/container_src/index.ts +74 -129
  11. package/container_src/isolation.ts +1039 -0
  12. package/container_src/jupyter-service.ts +8 -5
  13. package/container_src/shell-escape.ts +42 -0
  14. package/container_src/types.ts +35 -12
  15. package/dist/{chunk-VTKZL632.js → chunk-BEQUGUY4.js} +2 -2
  16. package/dist/{chunk-4KELYYKS.js → chunk-GTGWAEED.js} +239 -265
  17. package/dist/chunk-GTGWAEED.js.map +1 -0
  18. package/dist/{chunk-CUHYLCMT.js → chunk-SMUEY5JR.js} +111 -99
  19. package/dist/chunk-SMUEY5JR.js.map +1 -0
  20. package/dist/{client-bzEV222a.d.ts → client-Dny_ro_v.d.ts} +48 -84
  21. package/dist/client.d.ts +1 -1
  22. package/dist/client.js +1 -1
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +8 -9
  25. package/dist/interpreter.d.ts +2 -2
  26. package/dist/jupyter-client.d.ts +2 -2
  27. package/dist/jupyter-client.js +2 -2
  28. package/dist/request-handler.d.ts +3 -3
  29. package/dist/request-handler.js +3 -5
  30. package/dist/sandbox.d.ts +2 -2
  31. package/dist/sandbox.js +3 -5
  32. package/dist/types.d.ts +127 -21
  33. package/dist/types.js +35 -9
  34. package/dist/types.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/client.ts +175 -187
  37. package/src/index.ts +23 -13
  38. package/src/sandbox.ts +297 -332
  39. package/src/types.ts +125 -24
  40. package/dist/chunk-4KELYYKS.js.map +0 -1
  41. package/dist/chunk-CUHYLCMT.js.map +0 -1
  42. package/dist/chunk-S5FFBU4Y.js +0 -46
  43. package/dist/chunk-S5FFBU4Y.js.map +0 -1
  44. /package/dist/{chunk-VTKZL632.js.map → chunk-BEQUGUY4.js.map} +0 -0
@@ -1,615 +1,550 @@
1
- import { type SpawnOptions, spawn } from "node:child_process";
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
- // Generate a unique process ID using cryptographically secure randomness
6
- function generateProcessId(): string {
7
- return `proc_${Date.now()}_${randomBytes(6).toString('hex')}`;
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 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
- }
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
- 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
- }
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 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 || "/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
- 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;
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 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
- }
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
- 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
- },
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
- } 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,
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
- 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
- }
219
+ if (!sessionManager) {
220
+ return createErrorResponse(
221
+ "Session manager is required",
222
+ undefined,
223
+ 500,
224
+ corsHeaders
260
225
  );
261
226
  }
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
- );
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 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
- }
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
- 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
- }
260
+ if (!sessionManager) {
261
+ return createErrorResponse(
262
+ "Session manager is required",
263
+ undefined,
264
+ 500,
265
+ corsHeaders
322
266
  );
323
267
  }
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}`);
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 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
- }
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
- 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);
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 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
- );
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 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
- }
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
- 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
- }
371
+ if (!sessionManager) {
372
+ return createErrorResponse(
373
+ "Session manager is required",
374
+ undefined,
375
+ 500,
376
+ corsHeaders
461
377
  );
462
378
  }
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
- );
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 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
- }
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
- const record = processes.get(processId);
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
- 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
- }
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
- // Create a readable stream for Server-Sent Events
520
- let isConnected = true;
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 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,
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: record.sessionId
532
- })}\n\n`;
533
- controller.enqueue(new TextEncoder().encode(event));
474
+ sessionId,
475
+ timestamp: new Date().toISOString()
476
+ })}\n\n`));
534
477
  }
535
-
536
- if (record.stderr) {
537
- const event = `data: ${JSON.stringify({
538
- type: 'stderr',
539
- timestamp: new Date().toISOString(),
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: record.sessionId
543
- })}\n\n`;
544
- controller.enqueue(new TextEncoder().encode(event));
484
+ sessionId,
485
+ timestamp: new Date().toISOString()
486
+ })}\n\n`));
545
487
  }
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(),
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: 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
- }
509
+ sessionId,
510
+ timestamp: new Date().toISOString()
511
+ })}\n\n`));
575
512
  };
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
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
- // Add listeners
606
- record.outputListeners.add(outputListener);
607
- record.statusListeners.add(statusListener);
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
- isConnected = false;
612
- console.log(`[Server] Log stream cancelled for process ${processId}`);
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 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
- }
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
+ }