@mako10k/shell-server 0.2.2 → 0.2.4
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/README.md +35 -10
- package/dist/backoffice/server.js +9 -9
- package/dist/backoffice/server.js.map +1 -1
- package/dist/core/file-manager.d.ts +3 -3
- package/dist/core/file-manager.js +27 -27
- package/dist/core/file-manager.js.map +1 -1
- package/dist/core/file-storage-subscriber.d.ts +7 -7
- package/dist/core/file-storage-subscriber.js +19 -19
- package/dist/core/file-storage-subscriber.js.map +1 -1
- package/dist/core/monitoring-manager.d.ts +1 -1
- package/dist/core/monitoring-manager.js +31 -31
- package/dist/core/monitoring-manager.js.map +1 -1
- package/dist/core/process-manager.d.ts +4 -4
- package/dist/core/process-manager.js +151 -151
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/realtime-stream-subscriber.d.ts +18 -18
- package/dist/core/realtime-stream-subscriber.d.ts.map +1 -1
- package/dist/core/realtime-stream-subscriber.js +23 -23
- package/dist/core/realtime-stream-subscriber.js.map +1 -1
- package/dist/core/stream-publisher.d.ts +20 -20
- package/dist/core/stream-publisher.d.ts.map +1 -1
- package/dist/core/stream-publisher.js +15 -15
- package/dist/core/stream-publisher.js.map +1 -1
- package/dist/core/streaming-pipeline-reader.d.ts +12 -16
- package/dist/core/streaming-pipeline-reader.d.ts.map +1 -1
- package/dist/core/streaming-pipeline-reader.js +28 -28
- package/dist/core/streaming-pipeline-reader.js.map +1 -1
- package/dist/core/terminal-manager.d.ts +3 -3
- package/dist/core/terminal-manager.js +61 -61
- package/dist/core/terminal-manager.js.map +1 -1
- package/dist/executor/server.js +3 -3
- package/dist/executor/server.js.map +1 -1
- package/dist/security/chat-completion-adapter.d.ts.map +1 -1
- package/dist/security/chat-completion-adapter.js +23 -4
- package/dist/security/chat-completion-adapter.js.map +1 -1
- package/dist/security/enhanced-evaluator.js +3 -3
- package/dist/security/enhanced-evaluator.js.map +1 -1
- package/dist/security/manager.js +2 -2
- package/dist/security/manager.js.map +1 -1
- package/dist/security/security-llm-prompt-generator.d.ts +2 -2
- package/dist/security/security-llm-prompt-generator.js +4 -4
- package/dist/security/security-llm-prompt-generator.js.map +1 -1
- package/dist/tools/shell-tools.d.ts.map +1 -1
- package/dist/tools/shell-tools.js +28 -28
- package/dist/tools/shell-tools.js.map +1 -1
- package/dist/types/enhanced-security.js +29 -29
- package/dist/types/enhanced-security.js.map +1 -1
- package/dist/types/index.js +21 -21
- package/dist/types/index.js.map +1 -1
- package/dist/types/quick-schemas.js +5 -5
- package/dist/types/quick-schemas.js.map +1 -1
- package/dist/types/response-schemas.js +4 -4
- package/dist/types/response-schemas.js.map +1 -1
- package/dist/types/schemas.js +3 -3
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/errors.js +1 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/helpers.js +51 -51
- package/dist/utils/helpers.js.map +1 -1
- package/dist/utils/process-utils.d.ts +6 -6
- package/dist/utils/process-utils.js +29 -29
- package/dist/utils/process-utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -12,12 +12,12 @@ export class ProcessManager {
|
|
|
12
12
|
processes = new Map();
|
|
13
13
|
maxConcurrentProcesses;
|
|
14
14
|
outputDir;
|
|
15
|
-
terminalManager; // TerminalManager
|
|
16
|
-
fileManager; // FileManager
|
|
15
|
+
terminalManager; // Reference to TerminalManager
|
|
16
|
+
fileManager; // Reference to FileManager
|
|
17
17
|
defaultWorkingDirectory;
|
|
18
18
|
allowedWorkingDirectories;
|
|
19
|
-
backgroundProcessCallbacks = {}; //
|
|
20
|
-
// Issue #13: PUB/SUB
|
|
19
|
+
backgroundProcessCallbacks = {}; // Background process completion callbacks
|
|
20
|
+
// Issue #13: PUB/SUB integration - phased rollout with feature flag
|
|
21
21
|
streamPublisher;
|
|
22
22
|
fileStorageSubscriber;
|
|
23
23
|
realtimeStreamSubscriber;
|
|
@@ -30,45 +30,45 @@ export class ProcessManager {
|
|
|
30
30
|
this.allowedWorkingDirectories = process.env['SHELL_SERVER_ALLOWED_WORKDIRS']
|
|
31
31
|
? process.env['SHELL_SERVER_ALLOWED_WORKDIRS'].split(',').map((dir) => dir.trim())
|
|
32
32
|
: [process.cwd()];
|
|
33
|
-
// StreamPublisher
|
|
33
|
+
// Initialize StreamPublisher
|
|
34
34
|
this.streamPublisher = new StreamPublisher({
|
|
35
|
-
enableRealtimeStreaming: false, //
|
|
35
|
+
enableRealtimeStreaming: false, // disabled by default
|
|
36
36
|
bufferSize: 8192,
|
|
37
37
|
notificationInterval: 100,
|
|
38
38
|
});
|
|
39
|
-
//
|
|
39
|
+
// Control streaming via environment variable (phased rollout, enabled by default)
|
|
40
40
|
this.enableStreaming = process.env['SHELL_SERVER_ENABLE_STREAMING'] !== 'false';
|
|
41
41
|
if (this.enableStreaming) {
|
|
42
42
|
this.initializeStreamingComponents();
|
|
43
43
|
}
|
|
44
44
|
this.initializeOutputDirectory();
|
|
45
45
|
}
|
|
46
|
-
// TerminalManager
|
|
46
|
+
// Set TerminalManager reference
|
|
47
47
|
setTerminalManager(terminalManager) {
|
|
48
48
|
this.terminalManager = terminalManager;
|
|
49
49
|
}
|
|
50
|
-
// FileManager
|
|
50
|
+
// Set FileManager reference
|
|
51
51
|
setFileManager(fileManager) {
|
|
52
52
|
this.fileManager = fileManager;
|
|
53
|
-
// FileManager
|
|
53
|
+
// Reinitialize streaming when FileManager is set
|
|
54
54
|
if (this.enableStreaming) {
|
|
55
55
|
this.initializeStreamingComponents();
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
//
|
|
58
|
+
// Set callbacks for background process completion
|
|
59
59
|
setBackgroundProcessCallbacks(callbacks) {
|
|
60
60
|
this.backgroundProcessCallbacks = callbacks;
|
|
61
61
|
}
|
|
62
|
-
// Issue #13:
|
|
62
|
+
// Issue #13: Initialize streaming components
|
|
63
63
|
initializeStreamingComponents() {
|
|
64
64
|
if (!this.fileManager) {
|
|
65
65
|
console.error('ProcessManager: FileManager is required for streaming components');
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
// FileStorageSubscriber
|
|
68
|
+
// Initialize FileStorageSubscriber (replacing part of existing FileManager handling)
|
|
69
69
|
this.fileStorageSubscriber = new FileStorageSubscriber(this.fileManager, this.outputDir);
|
|
70
70
|
this.streamPublisher.subscribe(this.fileStorageSubscriber);
|
|
71
|
-
// RealtimeStreamSubscriber
|
|
71
|
+
// Initialize RealtimeStreamSubscriber
|
|
72
72
|
this.realtimeStreamSubscriber = new RealtimeStreamSubscriber({
|
|
73
73
|
bufferSize: 8192,
|
|
74
74
|
notificationInterval: 100,
|
|
@@ -78,14 +78,14 @@ export class ProcessManager {
|
|
|
78
78
|
this.streamPublisher.subscribe(this.realtimeStreamSubscriber);
|
|
79
79
|
console.error('ProcessManager: Streaming components initialized');
|
|
80
80
|
}
|
|
81
|
-
// Issue #13:
|
|
81
|
+
// Issue #13: Enable/disable streaming
|
|
82
82
|
enableStreamingFeature(enable = true) {
|
|
83
83
|
this.enableStreaming = enable;
|
|
84
84
|
if (enable && this.fileManager) {
|
|
85
85
|
this.initializeStreamingComponents();
|
|
86
86
|
}
|
|
87
87
|
else if (!enable) {
|
|
88
|
-
//
|
|
88
|
+
// Cleanup when streaming is disabled
|
|
89
89
|
if (this.realtimeStreamSubscriber) {
|
|
90
90
|
this.streamPublisher.unsubscribe(this.realtimeStreamSubscriber.id);
|
|
91
91
|
this.realtimeStreamSubscriber.destroy();
|
|
@@ -97,12 +97,12 @@ export class ProcessManager {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
// Issue #13: RealtimeStreamSubscriber
|
|
100
|
+
// Issue #13: Get RealtimeStreamSubscriber reference (for new MCP tools)
|
|
101
101
|
getRealtimeStreamSubscriber() {
|
|
102
102
|
return this.realtimeStreamSubscriber;
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
|
-
|
|
105
|
+
* Issue #13: Get execution ID from output_id
|
|
106
106
|
*/
|
|
107
107
|
findExecutionIdByOutputId(outputId) {
|
|
108
108
|
return this.fileManager?.getExecutionIdByOutputId(outputId);
|
|
@@ -111,12 +111,12 @@ export class ProcessManager {
|
|
|
111
111
|
await ensureDirectory(this.outputDir);
|
|
112
112
|
}
|
|
113
113
|
async executeCommand(options) {
|
|
114
|
-
//
|
|
114
|
+
// Check concurrent execution limit
|
|
115
115
|
const runningProcesses = Array.from(this.executions.values()).filter((exec) => exec.status === 'running').length;
|
|
116
116
|
if (runningProcesses >= this.maxConcurrentProcesses) {
|
|
117
117
|
throw new ResourceLimitError('concurrent processes', this.maxConcurrentProcesses);
|
|
118
118
|
}
|
|
119
|
-
//
|
|
119
|
+
// Prepare input data when input_output_id is specified
|
|
120
120
|
let resolvedInputData = options.inputData;
|
|
121
121
|
let inputStream = undefined;
|
|
122
122
|
if (options.inputOutputId) {
|
|
@@ -125,21 +125,21 @@ export class ProcessManager {
|
|
|
125
125
|
inputOutputId: options.inputOutputId,
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
|
-
// output_id
|
|
128
|
+
// Identify execution ID from output_id
|
|
129
129
|
const sourceExecutionId = this.findExecutionIdByOutputId(options.inputOutputId);
|
|
130
130
|
if (sourceExecutionId && this.realtimeStreamSubscriber) {
|
|
131
|
-
//
|
|
131
|
+
// For active processes: use StreamingPipelineReader
|
|
132
132
|
const streamState = this.realtimeStreamSubscriber.getStreamState(sourceExecutionId);
|
|
133
133
|
if (streamState && streamState.isActive) {
|
|
134
134
|
console.error(`ProcessManager: Using streaming pipeline for active process ${sourceExecutionId}`);
|
|
135
135
|
inputStream = new StreamingPipelineReader(this.fileManager, this.realtimeStreamSubscriber, options.inputOutputId, sourceExecutionId);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
//
|
|
138
|
+
// If not active (or on failure), fall back to traditional file read
|
|
139
139
|
if (!inputStream) {
|
|
140
140
|
try {
|
|
141
141
|
console.error(`ProcessManager: Using traditional file read for ${options.inputOutputId}`);
|
|
142
|
-
const result = await this.fileManager.readFile(options.inputOutputId, 0, 100 * 1024 * 1024, // 100MB
|
|
142
|
+
const result = await this.fileManager.readFile(options.inputOutputId, 0, 100 * 1024 * 1024, // read up to 100MB
|
|
143
143
|
'utf-8');
|
|
144
144
|
resolvedInputData = result.content;
|
|
145
145
|
}
|
|
@@ -153,7 +153,7 @@ export class ProcessManager {
|
|
|
153
153
|
}
|
|
154
154
|
const executionId = generateId();
|
|
155
155
|
const startTime = getCurrentTimestamp();
|
|
156
|
-
//
|
|
156
|
+
// Initialize execution info
|
|
157
157
|
const resolvedWorkingDirectory = this.resolveWorkingDirectory(options.workingDirectory);
|
|
158
158
|
const executionInfo = {
|
|
159
159
|
execution_id: executionId,
|
|
@@ -169,7 +169,7 @@ export class ProcessManager {
|
|
|
169
169
|
executionInfo.environment_variables = options.environmentVariables;
|
|
170
170
|
}
|
|
171
171
|
this.executions.set(executionId, executionInfo);
|
|
172
|
-
//
|
|
172
|
+
// If new terminal creation is requested
|
|
173
173
|
if (options.createTerminal && this.terminalManager) {
|
|
174
174
|
try {
|
|
175
175
|
const terminalOptions = {
|
|
@@ -186,9 +186,9 @@ export class ProcessManager {
|
|
|
186
186
|
}
|
|
187
187
|
const terminalInfo = await this.terminalManager.createTerminal(terminalOptions);
|
|
188
188
|
executionInfo.terminal_id = terminalInfo.terminal_id;
|
|
189
|
-
//
|
|
189
|
+
// Send command to terminal
|
|
190
190
|
this.terminalManager.sendInput(terminalInfo.terminal_id, options.command, true);
|
|
191
|
-
//
|
|
191
|
+
// Update execution info
|
|
192
192
|
executionInfo.status = 'completed';
|
|
193
193
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
194
194
|
this.executions.set(executionId, executionInfo);
|
|
@@ -204,13 +204,13 @@ export class ProcessManager {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
try {
|
|
207
|
-
//
|
|
207
|
+
// Prepare execution options
|
|
208
208
|
const { inputOutputId: _inputOutputId, ...baseOptions } = options;
|
|
209
209
|
const updatedOptions = {
|
|
210
210
|
...baseOptions,
|
|
211
211
|
...(resolvedInputData !== undefined && { inputData: resolvedInputData }),
|
|
212
212
|
};
|
|
213
|
-
// StreamingPipelineReader
|
|
213
|
+
// Special handling when StreamingPipelineReader exists
|
|
214
214
|
if (inputStream) {
|
|
215
215
|
return await this.executeCommandWithInputStream(executionId, updatedOptions, inputStream);
|
|
216
216
|
}
|
|
@@ -228,7 +228,7 @@ export class ProcessManager {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
catch (error) {
|
|
231
|
-
//
|
|
231
|
+
// Update execution info on error
|
|
232
232
|
const updatedInfo = this.executions.get(executionId);
|
|
233
233
|
if (updatedInfo) {
|
|
234
234
|
updatedInfo.status = 'failed';
|
|
@@ -239,7 +239,7 @@ export class ProcessManager {
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
/**
|
|
242
|
-
|
|
242
|
+
* Issue #13: Execute command using StreamingPipelineReader
|
|
243
243
|
*/
|
|
244
244
|
async executeCommandWithInputStream(executionId, options, inputStream) {
|
|
245
245
|
console.error(`ProcessManager: Executing command with input stream for ${executionId}`);
|
|
@@ -248,15 +248,15 @@ export class ProcessManager {
|
|
|
248
248
|
let stdout = '';
|
|
249
249
|
let stderr = '';
|
|
250
250
|
let outputTruncated = false;
|
|
251
|
-
//
|
|
251
|
+
// Prepare environment variables
|
|
252
252
|
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
253
|
-
//
|
|
253
|
+
// Start process
|
|
254
254
|
const child = spawn('sh', ['-c', options.command], {
|
|
255
255
|
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
256
256
|
env,
|
|
257
257
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
258
258
|
});
|
|
259
|
-
// StreamingPipelineReader
|
|
259
|
+
// Connect StreamingPipelineReader to STDIN
|
|
260
260
|
if (child.stdin) {
|
|
261
261
|
inputStream.pipe(child.stdin);
|
|
262
262
|
}
|
|
@@ -264,11 +264,11 @@ export class ProcessManager {
|
|
|
264
264
|
console.error(`StreamingPipelineReader error for ${executionId}: ${error.message}`);
|
|
265
265
|
child.kill('SIGTERM');
|
|
266
266
|
});
|
|
267
|
-
// StreamPublisher
|
|
267
|
+
// Notify StreamPublisher
|
|
268
268
|
if (this.streamPublisher) {
|
|
269
269
|
this.streamPublisher.notifyProcessStart(executionId, options.command);
|
|
270
270
|
}
|
|
271
|
-
// STDOUT
|
|
271
|
+
// Handle STDOUT
|
|
272
272
|
if (child.stdout) {
|
|
273
273
|
child.stdout.on('data', (data) => {
|
|
274
274
|
const chunk = data.toString();
|
|
@@ -278,13 +278,13 @@ export class ProcessManager {
|
|
|
278
278
|
else {
|
|
279
279
|
outputTruncated = true;
|
|
280
280
|
}
|
|
281
|
-
// StreamPublisher
|
|
281
|
+
// Notify StreamPublisher
|
|
282
282
|
if (this.streamPublisher) {
|
|
283
283
|
this.streamPublisher.notifyOutputData(executionId, chunk, false);
|
|
284
284
|
}
|
|
285
285
|
});
|
|
286
286
|
}
|
|
287
|
-
// STDERR
|
|
287
|
+
// Handle STDERR
|
|
288
288
|
if (options.captureStderr && child.stderr) {
|
|
289
289
|
child.stderr.on('data', (data) => {
|
|
290
290
|
const chunk = data.toString();
|
|
@@ -294,29 +294,29 @@ export class ProcessManager {
|
|
|
294
294
|
else {
|
|
295
295
|
outputTruncated = true;
|
|
296
296
|
}
|
|
297
|
-
// StreamPublisher
|
|
297
|
+
// Notify StreamPublisher
|
|
298
298
|
if (this.streamPublisher) {
|
|
299
299
|
this.streamPublisher.notifyOutputData(executionId, chunk, true);
|
|
300
300
|
}
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
|
-
//
|
|
303
|
+
// Handle process completion
|
|
304
304
|
child.on('close', async (code) => {
|
|
305
305
|
const executionInfo = this.executions.get(executionId);
|
|
306
306
|
if (!executionInfo) {
|
|
307
307
|
reject(new ExecutionError('Execution info not found', { executionId }));
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
|
-
//
|
|
310
|
+
// Calculate execution time
|
|
311
311
|
const executionTime = Date.now() - startTime;
|
|
312
|
-
//
|
|
312
|
+
// Update execution info
|
|
313
313
|
executionInfo.status = code === 0 ? 'completed' : 'failed';
|
|
314
314
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
315
315
|
if (code !== null) {
|
|
316
316
|
executionInfo.exit_code = code;
|
|
317
317
|
}
|
|
318
318
|
executionInfo.execution_time_ms = executionTime;
|
|
319
|
-
//
|
|
319
|
+
// Save output
|
|
320
320
|
if (this.fileManager) {
|
|
321
321
|
try {
|
|
322
322
|
const combinedOutput = stdout + (options.captureStderr ? stderr : '');
|
|
@@ -331,7 +331,7 @@ export class ProcessManager {
|
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
333
|
this.executions.set(executionId, executionInfo);
|
|
334
|
-
// StreamPublisher
|
|
334
|
+
// Notify StreamPublisher
|
|
335
335
|
if (this.streamPublisher) {
|
|
336
336
|
this.streamPublisher.notifyProcessEnd(executionId, code);
|
|
337
337
|
}
|
|
@@ -340,13 +340,13 @@ export class ProcessManager {
|
|
|
340
340
|
});
|
|
341
341
|
child.on('error', (error) => {
|
|
342
342
|
console.error(`Process error for ${executionId}: ${error.message}`);
|
|
343
|
-
// StreamPublisher
|
|
343
|
+
// Notify StreamPublisher
|
|
344
344
|
if (this.streamPublisher) {
|
|
345
345
|
this.streamPublisher.notifyError(executionId, error);
|
|
346
346
|
}
|
|
347
347
|
reject(new ExecutionError(`Process error: ${error.message}`, { originalError: String(error) }));
|
|
348
348
|
});
|
|
349
|
-
//
|
|
349
|
+
// Timeout handling
|
|
350
350
|
const timeout = setTimeout(() => {
|
|
351
351
|
console.error(`Process timeout for ${executionId}`);
|
|
352
352
|
child.kill('SIGTERM');
|
|
@@ -367,9 +367,9 @@ export class ProcessManager {
|
|
|
367
367
|
let stdout = '';
|
|
368
368
|
let stderr = '';
|
|
369
369
|
let outputTruncated = false;
|
|
370
|
-
//
|
|
370
|
+
// Prepare environment variables
|
|
371
371
|
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
372
|
-
//
|
|
372
|
+
// Start process
|
|
373
373
|
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
374
374
|
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
375
375
|
env,
|
|
@@ -378,7 +378,7 @@ export class ProcessManager {
|
|
|
378
378
|
if (childProcess.pid) {
|
|
379
379
|
this.processes.set(childProcess.pid, childProcess);
|
|
380
380
|
}
|
|
381
|
-
//
|
|
381
|
+
// Set timeout
|
|
382
382
|
const timeout = setTimeout(async () => {
|
|
383
383
|
childProcess.kill('SIGTERM');
|
|
384
384
|
setTimeout(() => {
|
|
@@ -397,21 +397,21 @@ export class ProcessManager {
|
|
|
397
397
|
if (childProcess.pid !== undefined) {
|
|
398
398
|
executionInfo.process_id = childProcess.pid;
|
|
399
399
|
}
|
|
400
|
-
//
|
|
400
|
+
// Save output to FileManager (regardless of size)
|
|
401
401
|
let outputFileId;
|
|
402
402
|
try {
|
|
403
403
|
outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
404
404
|
executionInfo.output_id = outputFileId;
|
|
405
405
|
}
|
|
406
406
|
catch (error) {
|
|
407
|
-
//
|
|
407
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
408
408
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
409
409
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
410
410
|
}
|
|
411
|
-
//
|
|
411
|
+
// Set detailed output status
|
|
412
412
|
this.setOutputStatus(executionInfo, outputTruncated, 'timeout', outputFileId);
|
|
413
413
|
this.executions.set(executionId, executionInfo);
|
|
414
|
-
// return_partial_on_timeout
|
|
414
|
+
// Return partial result when return_partial_on_timeout is true
|
|
415
415
|
if (options.returnPartialOnTimeout) {
|
|
416
416
|
resolve(executionInfo);
|
|
417
417
|
return;
|
|
@@ -419,7 +419,7 @@ export class ProcessManager {
|
|
|
419
419
|
}
|
|
420
420
|
reject(new TimeoutError(options.timeoutSeconds));
|
|
421
421
|
}, options.timeoutSeconds * 1000);
|
|
422
|
-
//
|
|
422
|
+
// Send stdin
|
|
423
423
|
if (options.inputData) {
|
|
424
424
|
childProcess.stdin?.write(options.inputData);
|
|
425
425
|
childProcess.stdin?.end();
|
|
@@ -427,7 +427,7 @@ export class ProcessManager {
|
|
|
427
427
|
else {
|
|
428
428
|
childProcess.stdin?.end();
|
|
429
429
|
}
|
|
430
|
-
//
|
|
430
|
+
// Handle stdout
|
|
431
431
|
childProcess.stdout?.on('data', (data) => {
|
|
432
432
|
const output = data.toString();
|
|
433
433
|
if (stdout.length + output.length <= options.maxOutputSize) {
|
|
@@ -438,7 +438,7 @@ export class ProcessManager {
|
|
|
438
438
|
outputTruncated = true;
|
|
439
439
|
}
|
|
440
440
|
});
|
|
441
|
-
//
|
|
441
|
+
// Handle stderr
|
|
442
442
|
if (options.captureStderr) {
|
|
443
443
|
childProcess.stderr?.on('data', (data) => {
|
|
444
444
|
const output = data.toString();
|
|
@@ -451,7 +451,7 @@ export class ProcessManager {
|
|
|
451
451
|
}
|
|
452
452
|
});
|
|
453
453
|
}
|
|
454
|
-
//
|
|
454
|
+
// Handle process close
|
|
455
455
|
childProcess.on('close', async (code) => {
|
|
456
456
|
clearTimeout(timeout);
|
|
457
457
|
if (childProcess.pid) {
|
|
@@ -469,30 +469,30 @@ export class ProcessManager {
|
|
|
469
469
|
executionInfo.process_id = childProcess.pid;
|
|
470
470
|
}
|
|
471
471
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
472
|
-
//
|
|
472
|
+
// Save output to FileManager (regardless of size)
|
|
473
473
|
let outputFileId;
|
|
474
474
|
try {
|
|
475
475
|
outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
476
476
|
executionInfo.output_id = outputFileId;
|
|
477
477
|
}
|
|
478
478
|
catch (error) {
|
|
479
|
-
//
|
|
479
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
480
480
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
481
481
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
482
482
|
}
|
|
483
|
-
//
|
|
483
|
+
// Set detailed output status
|
|
484
484
|
if (outputTruncated) {
|
|
485
485
|
this.setOutputStatus(executionInfo, true, 'size_limit', outputFileId);
|
|
486
486
|
}
|
|
487
487
|
else {
|
|
488
|
-
//
|
|
488
|
+
// Normal completion: actuallyTruncated=false, use a valid reason to show completion guidance
|
|
489
489
|
this.setOutputStatus(executionInfo, false, 'size_limit', outputFileId);
|
|
490
490
|
}
|
|
491
491
|
this.executions.set(executionId, executionInfo);
|
|
492
492
|
resolve(executionInfo);
|
|
493
493
|
}
|
|
494
494
|
});
|
|
495
|
-
//
|
|
495
|
+
// Error handling
|
|
496
496
|
childProcess.on('error', (error) => {
|
|
497
497
|
clearTimeout(timeout);
|
|
498
498
|
if (childProcess.pid) {
|
|
@@ -512,9 +512,9 @@ export class ProcessManager {
|
|
|
512
512
|
});
|
|
513
513
|
}
|
|
514
514
|
async executeAdaptiveCommand(executionId, options) {
|
|
515
|
-
//
|
|
516
|
-
// 1.
|
|
517
|
-
// 2.
|
|
515
|
+
// Adaptive mode: start one process and transition to background when:
|
|
516
|
+
// 1. Foreground timeout is reached
|
|
517
|
+
// 2. Output size limit is reached
|
|
518
518
|
const returnPartialOnTimeout = options.returnPartialOnTimeout ?? true;
|
|
519
519
|
const foregroundTimeout = options.foregroundTimeoutSeconds ?? 10;
|
|
520
520
|
return new Promise((resolve, reject) => {
|
|
@@ -523,9 +523,9 @@ export class ProcessManager {
|
|
|
523
523
|
let stderr = '';
|
|
524
524
|
let outputTruncated = false;
|
|
525
525
|
let backgroundTransitionReason = null;
|
|
526
|
-
//
|
|
526
|
+
// Prepare environment variables
|
|
527
527
|
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
528
|
-
//
|
|
528
|
+
// Start process (supports background transition)
|
|
529
529
|
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
530
530
|
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
531
531
|
env,
|
|
@@ -534,14 +534,14 @@ export class ProcessManager {
|
|
|
534
534
|
if (childProcess.pid) {
|
|
535
535
|
this.processes.set(childProcess.pid, childProcess);
|
|
536
536
|
}
|
|
537
|
-
//
|
|
537
|
+
// Set foreground timeout
|
|
538
538
|
const foregroundTimeoutHandle = setTimeout(() => {
|
|
539
539
|
if (!backgroundTransitionReason) {
|
|
540
540
|
backgroundTransitionReason = 'timeout';
|
|
541
541
|
transitionToBackground();
|
|
542
542
|
}
|
|
543
543
|
}, foregroundTimeout * 1000);
|
|
544
|
-
//
|
|
544
|
+
// Set final timeout
|
|
545
545
|
const finalTimeoutHandle = setTimeout(async () => {
|
|
546
546
|
childProcess.kill('SIGTERM');
|
|
547
547
|
setTimeout(() => {
|
|
@@ -557,13 +557,13 @@ export class ProcessManager {
|
|
|
557
557
|
executionInfo.output_truncated = outputTruncated;
|
|
558
558
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
559
559
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
560
|
-
//
|
|
560
|
+
// Save output to FileManager
|
|
561
561
|
try {
|
|
562
562
|
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
563
563
|
executionInfo.output_id = outputFileId;
|
|
564
564
|
}
|
|
565
565
|
catch (error) {
|
|
566
|
-
//
|
|
566
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
567
567
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
568
568
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
569
569
|
}
|
|
@@ -575,7 +575,7 @@ export class ProcessManager {
|
|
|
575
575
|
}
|
|
576
576
|
reject(new TimeoutError(options.timeoutSeconds));
|
|
577
577
|
}, options.timeoutSeconds * 1000);
|
|
578
|
-
//
|
|
578
|
+
// Function to transition to background mode
|
|
579
579
|
const transitionToBackground = async () => {
|
|
580
580
|
clearTimeout(foregroundTimeoutHandle);
|
|
581
581
|
const executionInfo = this.executions.get(executionId);
|
|
@@ -583,7 +583,7 @@ export class ProcessManager {
|
|
|
583
583
|
executionInfo.status = 'running';
|
|
584
584
|
executionInfo.stdout = sanitizeString(stdout);
|
|
585
585
|
executionInfo.stderr = sanitizeString(stderr);
|
|
586
|
-
//
|
|
586
|
+
// Record transition reason
|
|
587
587
|
if (backgroundTransitionReason === 'timeout') {
|
|
588
588
|
executionInfo.transition_reason = 'foreground_timeout';
|
|
589
589
|
}
|
|
@@ -593,21 +593,21 @@ export class ProcessManager {
|
|
|
593
593
|
if (childProcess.pid !== undefined) {
|
|
594
594
|
executionInfo.process_id = childProcess.pid;
|
|
595
595
|
}
|
|
596
|
-
//
|
|
596
|
+
// Save output to FileManager
|
|
597
597
|
let outputFileId;
|
|
598
598
|
try {
|
|
599
599
|
outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
600
600
|
executionInfo.output_id = outputFileId;
|
|
601
601
|
}
|
|
602
602
|
catch (error) {
|
|
603
|
-
//
|
|
603
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
604
604
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
605
605
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
606
606
|
}
|
|
607
|
-
//
|
|
607
|
+
// Set detailed output status (background transition)
|
|
608
608
|
this.setOutputStatus(executionInfo, outputTruncated, 'background_transition', outputFileId);
|
|
609
609
|
this.executions.set(executionId, executionInfo);
|
|
610
|
-
//
|
|
610
|
+
// Configure continued background handling (adaptive mode only)
|
|
611
611
|
this.handleAdaptiveBackgroundTransition(executionId, childProcess, {
|
|
612
612
|
...options,
|
|
613
613
|
timeoutSeconds: Math.max(1, options.timeoutSeconds - Math.floor((Date.now() - startTime) / 1000)),
|
|
@@ -615,7 +615,7 @@ export class ProcessManager {
|
|
|
615
615
|
resolve(executionInfo);
|
|
616
616
|
}
|
|
617
617
|
};
|
|
618
|
-
//
|
|
618
|
+
// Send stdin
|
|
619
619
|
if (options.inputData) {
|
|
620
620
|
childProcess.stdin?.write(options.inputData);
|
|
621
621
|
childProcess.stdin?.end();
|
|
@@ -623,7 +623,7 @@ export class ProcessManager {
|
|
|
623
623
|
else {
|
|
624
624
|
childProcess.stdin?.end();
|
|
625
625
|
}
|
|
626
|
-
//
|
|
626
|
+
// Handle stdout
|
|
627
627
|
childProcess.stdout?.on('data', (data) => {
|
|
628
628
|
const output = data.toString();
|
|
629
629
|
if (stdout.length + output.length <= options.maxOutputSize) {
|
|
@@ -632,14 +632,14 @@ export class ProcessManager {
|
|
|
632
632
|
else {
|
|
633
633
|
stdout += output.substring(0, options.maxOutputSize - stdout.length);
|
|
634
634
|
outputTruncated = true;
|
|
635
|
-
//
|
|
635
|
+
// Transition to background when output size limit is reached
|
|
636
636
|
if (!backgroundTransitionReason) {
|
|
637
637
|
backgroundTransitionReason = 'output_size_limit';
|
|
638
638
|
transitionToBackground();
|
|
639
639
|
}
|
|
640
640
|
}
|
|
641
641
|
});
|
|
642
|
-
//
|
|
642
|
+
// Handle stderr
|
|
643
643
|
if (options.captureStderr) {
|
|
644
644
|
childProcess.stderr?.on('data', (data) => {
|
|
645
645
|
const output = data.toString();
|
|
@@ -649,7 +649,7 @@ export class ProcessManager {
|
|
|
649
649
|
else {
|
|
650
650
|
stderr += output.substring(0, options.maxOutputSize - stderr.length);
|
|
651
651
|
outputTruncated = true;
|
|
652
|
-
//
|
|
652
|
+
// Transition to background when output size limit is reached
|
|
653
653
|
if (!backgroundTransitionReason) {
|
|
654
654
|
backgroundTransitionReason = 'output_size_limit';
|
|
655
655
|
transitionToBackground();
|
|
@@ -657,14 +657,14 @@ export class ProcessManager {
|
|
|
657
657
|
}
|
|
658
658
|
});
|
|
659
659
|
}
|
|
660
|
-
//
|
|
660
|
+
// Handle process close
|
|
661
661
|
childProcess.on('close', async (code) => {
|
|
662
662
|
clearTimeout(foregroundTimeoutHandle);
|
|
663
663
|
clearTimeout(finalTimeoutHandle);
|
|
664
664
|
if (childProcess.pid) {
|
|
665
665
|
this.processes.delete(childProcess.pid);
|
|
666
666
|
}
|
|
667
|
-
//
|
|
667
|
+
// Handle only when no background transition occurred
|
|
668
668
|
if (!backgroundTransitionReason) {
|
|
669
669
|
const executionTime = Date.now() - startTime;
|
|
670
670
|
const executionInfo = this.executions.get(executionId);
|
|
@@ -679,13 +679,13 @@ export class ProcessManager {
|
|
|
679
679
|
executionInfo.process_id = childProcess.pid;
|
|
680
680
|
}
|
|
681
681
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
682
|
-
//
|
|
682
|
+
// Save output to FileManager
|
|
683
683
|
try {
|
|
684
684
|
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
685
685
|
executionInfo.output_id = outputFileId;
|
|
686
686
|
}
|
|
687
687
|
catch (error) {
|
|
688
|
-
//
|
|
688
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
689
689
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
690
690
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
691
691
|
}
|
|
@@ -694,7 +694,7 @@ export class ProcessManager {
|
|
|
694
694
|
}
|
|
695
695
|
}
|
|
696
696
|
});
|
|
697
|
-
//
|
|
697
|
+
// Error handling
|
|
698
698
|
childProcess.on('error', (error) => {
|
|
699
699
|
clearTimeout(foregroundTimeoutHandle);
|
|
700
700
|
clearTimeout(finalTimeoutHandle);
|
|
@@ -732,7 +732,7 @@ export class ProcessManager {
|
|
|
732
732
|
executionInfo.process_id = childProcess.pid;
|
|
733
733
|
this.executions.set(executionId, executionInfo);
|
|
734
734
|
}
|
|
735
|
-
//
|
|
735
|
+
// For background processes, handle output asynchronously
|
|
736
736
|
if (options.executionMode === 'background') {
|
|
737
737
|
this.handleBackgroundProcess(executionId, childProcess, options);
|
|
738
738
|
}
|
|
@@ -746,7 +746,7 @@ export class ProcessManager {
|
|
|
746
746
|
const startTime = Date.now();
|
|
747
747
|
let stdout = '';
|
|
748
748
|
let stderr = '';
|
|
749
|
-
//
|
|
749
|
+
// Set timeout (for background processes)
|
|
750
750
|
const timeout = setTimeout(async () => {
|
|
751
751
|
childProcess.kill('SIGTERM');
|
|
752
752
|
setTimeout(() => {
|
|
@@ -762,18 +762,18 @@ export class ProcessManager {
|
|
|
762
762
|
executionInfo.output_truncated = true;
|
|
763
763
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
764
764
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
765
|
-
//
|
|
765
|
+
// Save output to FileManager
|
|
766
766
|
try {
|
|
767
767
|
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
768
768
|
executionInfo.output_id = outputFileId;
|
|
769
769
|
}
|
|
770
770
|
catch (error) {
|
|
771
|
-
//
|
|
771
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
772
772
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
773
773
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
774
774
|
}
|
|
775
775
|
this.executions.set(executionId, executionInfo);
|
|
776
|
-
//
|
|
776
|
+
// Invoke timeout callback for background process
|
|
777
777
|
if (this.backgroundProcessCallbacks.onTimeout) {
|
|
778
778
|
setImmediate(async () => {
|
|
779
779
|
try {
|
|
@@ -786,14 +786,14 @@ export class ProcessManager {
|
|
|
786
786
|
}
|
|
787
787
|
}
|
|
788
788
|
catch (callbackError) {
|
|
789
|
-
//
|
|
789
|
+
// Record callback errors in internal logs only
|
|
790
790
|
// console.error('Background process timeout callback error:', callbackError);
|
|
791
791
|
}
|
|
792
792
|
});
|
|
793
793
|
}
|
|
794
794
|
}
|
|
795
795
|
}, options.timeoutSeconds * 1000);
|
|
796
|
-
//
|
|
796
|
+
// Collect output
|
|
797
797
|
childProcess.stdout?.on('data', (data) => {
|
|
798
798
|
stdout += data.toString();
|
|
799
799
|
});
|
|
@@ -802,7 +802,7 @@ export class ProcessManager {
|
|
|
802
802
|
stderr += data.toString();
|
|
803
803
|
});
|
|
804
804
|
}
|
|
805
|
-
//
|
|
805
|
+
// Handle process close
|
|
806
806
|
childProcess.on('close', async (code) => {
|
|
807
807
|
clearTimeout(timeout);
|
|
808
808
|
if (childProcess.pid) {
|
|
@@ -814,18 +814,18 @@ export class ProcessManager {
|
|
|
814
814
|
executionInfo.exit_code = code || 0;
|
|
815
815
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
816
816
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
817
|
-
//
|
|
817
|
+
// Save output to file
|
|
818
818
|
try {
|
|
819
819
|
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
820
820
|
executionInfo.output_id = outputFileId;
|
|
821
821
|
}
|
|
822
822
|
catch (error) {
|
|
823
|
-
//
|
|
823
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
824
824
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
825
825
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
826
826
|
}
|
|
827
827
|
this.executions.set(executionId, executionInfo);
|
|
828
|
-
//
|
|
828
|
+
// Invoke completion callback for background process
|
|
829
829
|
if (this.backgroundProcessCallbacks.onComplete) {
|
|
830
830
|
setImmediate(async () => {
|
|
831
831
|
try {
|
|
@@ -838,7 +838,7 @@ export class ProcessManager {
|
|
|
838
838
|
}
|
|
839
839
|
}
|
|
840
840
|
catch (callbackError) {
|
|
841
|
-
//
|
|
841
|
+
// Record callback errors in internal logs only
|
|
842
842
|
// console.error('Background process completion callback error:', callbackError);
|
|
843
843
|
}
|
|
844
844
|
});
|
|
@@ -856,7 +856,7 @@ export class ProcessManager {
|
|
|
856
856
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
857
857
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
858
858
|
this.executions.set(executionId, executionInfo);
|
|
859
|
-
//
|
|
859
|
+
// Invoke error callback for background process
|
|
860
860
|
if (this.backgroundProcessCallbacks.onError) {
|
|
861
861
|
setImmediate(async () => {
|
|
862
862
|
try {
|
|
@@ -869,7 +869,7 @@ export class ProcessManager {
|
|
|
869
869
|
}
|
|
870
870
|
}
|
|
871
871
|
catch (callbackError) {
|
|
872
|
-
//
|
|
872
|
+
// Record callback errors in internal logs only
|
|
873
873
|
// console.error('Background process error callback error:', callbackError);
|
|
874
874
|
}
|
|
875
875
|
});
|
|
@@ -877,9 +877,9 @@ export class ProcessManager {
|
|
|
877
877
|
}
|
|
878
878
|
});
|
|
879
879
|
}
|
|
880
|
-
// adaptive mode
|
|
880
|
+
// Handle processes transitioned to background in adaptive mode
|
|
881
881
|
handleAdaptiveBackgroundTransition(executionId, childProcess, options) {
|
|
882
|
-
//
|
|
882
|
+
// Set timeout (final timeout)
|
|
883
883
|
const timeout = setTimeout(async () => {
|
|
884
884
|
childProcess.kill('SIGTERM');
|
|
885
885
|
setTimeout(() => {
|
|
@@ -891,11 +891,11 @@ export class ProcessManager {
|
|
|
891
891
|
if (executionInfo) {
|
|
892
892
|
executionInfo.status = 'timeout';
|
|
893
893
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
894
|
-
//
|
|
894
|
+
// Keep existing output (already captured in adaptive mode)
|
|
895
895
|
this.executions.set(executionId, executionInfo);
|
|
896
896
|
}
|
|
897
897
|
}, options.timeoutSeconds * 1000);
|
|
898
|
-
//
|
|
898
|
+
// Handle process close
|
|
899
899
|
childProcess.on('close', async (code) => {
|
|
900
900
|
clearTimeout(timeout);
|
|
901
901
|
if (childProcess.pid) {
|
|
@@ -906,13 +906,13 @@ export class ProcessManager {
|
|
|
906
906
|
executionInfo.status = 'completed';
|
|
907
907
|
executionInfo.exit_code = code || 0;
|
|
908
908
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
909
|
-
//
|
|
909
|
+
// Calculate total execution time (foreground + background)
|
|
910
910
|
if (executionInfo.started_at) {
|
|
911
911
|
const startTime = new Date(executionInfo.started_at).getTime();
|
|
912
912
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
913
913
|
}
|
|
914
914
|
this.executions.set(executionId, executionInfo);
|
|
915
|
-
// adaptive
|
|
915
|
+
// Invoke completion callback for adaptive-mode background process
|
|
916
916
|
if (this.backgroundProcessCallbacks.onComplete) {
|
|
917
917
|
setImmediate(async () => {
|
|
918
918
|
try {
|
|
@@ -925,7 +925,7 @@ export class ProcessManager {
|
|
|
925
925
|
}
|
|
926
926
|
}
|
|
927
927
|
catch (callbackError) {
|
|
928
|
-
//
|
|
928
|
+
// Record callback errors in internal logs only
|
|
929
929
|
// console.error('Adaptive background process completion callback error:', callbackError);
|
|
930
930
|
}
|
|
931
931
|
});
|
|
@@ -946,7 +946,7 @@ export class ProcessManager {
|
|
|
946
946
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
947
947
|
}
|
|
948
948
|
this.executions.set(executionId, executionInfo);
|
|
949
|
-
// adaptive
|
|
949
|
+
// Invoke error callback for adaptive-mode background process
|
|
950
950
|
if (this.backgroundProcessCallbacks.onError) {
|
|
951
951
|
setImmediate(async () => {
|
|
952
952
|
try {
|
|
@@ -959,7 +959,7 @@ export class ProcessManager {
|
|
|
959
959
|
}
|
|
960
960
|
}
|
|
961
961
|
catch (callbackError) {
|
|
962
|
-
//
|
|
962
|
+
// Record callback errors in internal logs only
|
|
963
963
|
// console.error('Adaptive background process error callback error:', callbackError);
|
|
964
964
|
}
|
|
965
965
|
});
|
|
@@ -968,23 +968,23 @@ export class ProcessManager {
|
|
|
968
968
|
});
|
|
969
969
|
}
|
|
970
970
|
async executeDetachedCommand(executionId, options) {
|
|
971
|
-
//
|
|
971
|
+
// Detached mode: run fully in background and detach from parent process
|
|
972
972
|
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
973
973
|
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
974
974
|
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
975
975
|
env,
|
|
976
|
-
stdio: ['ignore', 'pipe', 'pipe'], // stdin
|
|
977
|
-
detached: true, //
|
|
976
|
+
stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin
|
|
977
|
+
detached: true, // fully detached
|
|
978
978
|
});
|
|
979
|
-
//
|
|
979
|
+
// Record detached process PID but exclude it from process management
|
|
980
980
|
const executionInfo = this.executions.get(executionId);
|
|
981
981
|
if (executionInfo && childProcess.pid !== undefined) {
|
|
982
982
|
executionInfo.process_id = childProcess.pid;
|
|
983
983
|
executionInfo.status = 'running';
|
|
984
984
|
this.executions.set(executionId, executionInfo);
|
|
985
985
|
}
|
|
986
|
-
//
|
|
987
|
-
//
|
|
986
|
+
// Detached processes continue after parent exits,
|
|
987
|
+
// so output collection is limited
|
|
988
988
|
const startTime = Date.now();
|
|
989
989
|
let stdout = '';
|
|
990
990
|
let stderr = '';
|
|
@@ -998,7 +998,7 @@ export class ProcessManager {
|
|
|
998
998
|
stderr += data.toString();
|
|
999
999
|
});
|
|
1000
1000
|
}
|
|
1001
|
-
//
|
|
1001
|
+
// Monitor process exit (not always capturable when detached)
|
|
1002
1002
|
childProcess.on('close', async (code) => {
|
|
1003
1003
|
const executionInfo = this.executions.get(executionId);
|
|
1004
1004
|
if (executionInfo) {
|
|
@@ -1011,12 +1011,12 @@ export class ProcessManager {
|
|
|
1011
1011
|
executionInfo.output_id = outputFileId;
|
|
1012
1012
|
}
|
|
1013
1013
|
catch (error) {
|
|
1014
|
-
//
|
|
1014
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
1015
1015
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
1016
1016
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1017
1017
|
}
|
|
1018
1018
|
this.executions.set(executionId, executionInfo);
|
|
1019
|
-
// detached
|
|
1019
|
+
// Invoke completion callback for detached process
|
|
1020
1020
|
if (this.backgroundProcessCallbacks.onComplete) {
|
|
1021
1021
|
setImmediate(async () => {
|
|
1022
1022
|
try {
|
|
@@ -1029,7 +1029,7 @@ export class ProcessManager {
|
|
|
1029
1029
|
}
|
|
1030
1030
|
}
|
|
1031
1031
|
catch (callbackError) {
|
|
1032
|
-
//
|
|
1032
|
+
// Record callback errors in internal logs only
|
|
1033
1033
|
// console.error('Detached process completion callback error:', callbackError);
|
|
1034
1034
|
}
|
|
1035
1035
|
});
|
|
@@ -1043,7 +1043,7 @@ export class ProcessManager {
|
|
|
1043
1043
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
1044
1044
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
1045
1045
|
this.executions.set(executionId, executionInfo);
|
|
1046
|
-
// detached
|
|
1046
|
+
// Invoke error callback for detached process
|
|
1047
1047
|
if (this.backgroundProcessCallbacks.onError) {
|
|
1048
1048
|
setImmediate(async () => {
|
|
1049
1049
|
try {
|
|
@@ -1056,14 +1056,14 @@ export class ProcessManager {
|
|
|
1056
1056
|
}
|
|
1057
1057
|
}
|
|
1058
1058
|
catch (callbackError) {
|
|
1059
|
-
//
|
|
1059
|
+
// Record callback errors in internal logs only
|
|
1060
1060
|
// console.error('Detached process error callback error:', callbackError);
|
|
1061
1061
|
}
|
|
1062
1062
|
});
|
|
1063
1063
|
}
|
|
1064
1064
|
}
|
|
1065
1065
|
});
|
|
1066
|
-
//
|
|
1066
|
+
// Detach process
|
|
1067
1067
|
childProcess.unref();
|
|
1068
1068
|
const resultExecutionInfo = this.executions.get(executionId);
|
|
1069
1069
|
if (!resultExecutionInfo) {
|
|
@@ -1073,7 +1073,7 @@ export class ProcessManager {
|
|
|
1073
1073
|
}
|
|
1074
1074
|
async saveOutputToFile(executionId, stdout, stderr) {
|
|
1075
1075
|
if (!this.fileManager) {
|
|
1076
|
-
// FileManager
|
|
1076
|
+
// If FileManager is unavailable, save file using legacy method
|
|
1077
1077
|
const outputFileId = generateId();
|
|
1078
1078
|
const filePath = path.join(this.outputDir, `${outputFileId}.json`);
|
|
1079
1079
|
const outputData = {
|
|
@@ -1085,27 +1085,27 @@ export class ProcessManager {
|
|
|
1085
1085
|
await fs.writeFile(filePath, JSON.stringify(outputData, null, 2), 'utf-8');
|
|
1086
1086
|
return outputFileId;
|
|
1087
1087
|
}
|
|
1088
|
-
// FileManager
|
|
1088
|
+
// Create output file using FileManager
|
|
1089
1089
|
const combinedOutput = stdout + (stderr ? '\n--- STDERR ---\n' + stderr : '');
|
|
1090
1090
|
return await this.fileManager.createOutputFile(combinedOutput, executionId);
|
|
1091
1091
|
}
|
|
1092
1092
|
/**
|
|
1093
|
-
|
|
1093
|
+
* Helper to set detailed output status information
|
|
1094
1094
|
* Issue #14: Enhanced guidance messages for adaptive mode transitions
|
|
1095
|
-
|
|
1095
|
+
* Improvement: determine status by reason instead of outputTruncated
|
|
1096
1096
|
*/
|
|
1097
|
-
setOutputStatus(executionInfo, actuallyTruncated, //
|
|
1097
|
+
setOutputStatus(executionInfo, actuallyTruncated, // whether output was actually truncated
|
|
1098
1098
|
reason, outputId) {
|
|
1099
|
-
// reason
|
|
1100
|
-
const needsGuidance = !!outputId; // output_id
|
|
1101
|
-
//
|
|
1099
|
+
// Set output status based on reason
|
|
1100
|
+
const needsGuidance = !!outputId; // always provide guidance when output_id exists
|
|
1101
|
+
// Set outputTruncated for backward compatibility
|
|
1102
1102
|
executionInfo.output_truncated =
|
|
1103
1103
|
actuallyTruncated || reason === 'timeout' || reason === 'background_transition';
|
|
1104
|
-
// Issue #14:
|
|
1104
|
+
// Issue #14: Handle background transitions and timeouts specially
|
|
1105
1105
|
if (reason === 'background_transition') {
|
|
1106
1106
|
executionInfo.truncation_reason = reason;
|
|
1107
1107
|
executionInfo.output_status = {
|
|
1108
|
-
complete: false, //
|
|
1108
|
+
complete: false, // incomplete while running in background
|
|
1109
1109
|
reason: reason,
|
|
1110
1110
|
available_via_output_id: !!outputId,
|
|
1111
1111
|
recommended_action: outputId ? 'use_read_execution_output' : undefined,
|
|
@@ -1136,7 +1136,7 @@ export class ProcessManager {
|
|
|
1136
1136
|
if (reason === 'timeout') {
|
|
1137
1137
|
executionInfo.truncation_reason = reason;
|
|
1138
1138
|
executionInfo.output_status = {
|
|
1139
|
-
complete: false, //
|
|
1139
|
+
complete: false, // timeout means incomplete
|
|
1140
1140
|
reason: reason,
|
|
1141
1141
|
available_via_output_id: !!outputId,
|
|
1142
1142
|
recommended_action: outputId ? 'use_read_execution_output' : undefined,
|
|
@@ -1158,7 +1158,7 @@ export class ProcessManager {
|
|
|
1158
1158
|
}
|
|
1159
1159
|
return;
|
|
1160
1160
|
}
|
|
1161
|
-
//
|
|
1161
|
+
// When output was actually truncated
|
|
1162
1162
|
if (actuallyTruncated) {
|
|
1163
1163
|
executionInfo.truncation_reason = reason;
|
|
1164
1164
|
executionInfo.output_status = {
|
|
@@ -1167,7 +1167,7 @@ export class ProcessManager {
|
|
|
1167
1167
|
available_via_output_id: !!outputId,
|
|
1168
1168
|
recommended_action: outputId ? 'use_read_execution_output' : undefined,
|
|
1169
1169
|
};
|
|
1170
|
-
//
|
|
1170
|
+
// Set message and actions based on situation
|
|
1171
1171
|
switch (reason) {
|
|
1172
1172
|
case 'size_limit':
|
|
1173
1173
|
executionInfo.message = `Output exceeded size limit. ${outputId ? 'Complete output available via output_id.' : 'Output was truncated.'}`;
|
|
@@ -1205,7 +1205,7 @@ export class ProcessManager {
|
|
|
1205
1205
|
}
|
|
1206
1206
|
}
|
|
1207
1207
|
else {
|
|
1208
|
-
//
|
|
1208
|
+
// Completed case (no truncation)
|
|
1209
1209
|
executionInfo.output_status = {
|
|
1210
1210
|
complete: true,
|
|
1211
1211
|
available_via_output_id: !!outputId,
|
|
@@ -1228,7 +1228,7 @@ export class ProcessManager {
|
|
|
1228
1228
|
}
|
|
1229
1229
|
listExecutions(filter) {
|
|
1230
1230
|
let executions = Array.from(this.executions.values());
|
|
1231
|
-
//
|
|
1231
|
+
// Filtering
|
|
1232
1232
|
if (filter) {
|
|
1233
1233
|
if (filter.status) {
|
|
1234
1234
|
executions = executions.filter((exec) => exec.status === filter.status);
|
|
@@ -1238,11 +1238,11 @@ export class ProcessManager {
|
|
|
1238
1238
|
executions = executions.filter((exec) => pattern.test(exec.command));
|
|
1239
1239
|
}
|
|
1240
1240
|
if (filter.sessionId) {
|
|
1241
|
-
//
|
|
1241
|
+
// Session management will be implemented later
|
|
1242
1242
|
}
|
|
1243
1243
|
}
|
|
1244
1244
|
const total = executions.length;
|
|
1245
|
-
//
|
|
1245
|
+
// Pagination
|
|
1246
1246
|
if (filter?.offset || filter?.limit) {
|
|
1247
1247
|
const offset = filter.offset || 0;
|
|
1248
1248
|
const limit = filter.limit || 50;
|
|
@@ -1256,17 +1256,17 @@ export class ProcessManager {
|
|
|
1256
1256
|
throw new ResourceNotFoundError('process', processId.toString());
|
|
1257
1257
|
}
|
|
1258
1258
|
try {
|
|
1259
|
-
//
|
|
1259
|
+
// Terminate process
|
|
1260
1260
|
const signalName = signal === 'KILL' ? 'SIGKILL' : `SIG${signal}`;
|
|
1261
1261
|
const killed = childProcess.kill(signalName);
|
|
1262
1262
|
if (!killed && force && signal !== 'KILL') {
|
|
1263
|
-
//
|
|
1263
|
+
// Force kill
|
|
1264
1264
|
childProcess.kill('SIGKILL');
|
|
1265
1265
|
}
|
|
1266
|
-
//
|
|
1266
|
+
// Wait until process exits
|
|
1267
1267
|
await new Promise((resolve) => {
|
|
1268
1268
|
childProcess.on('close', () => resolve());
|
|
1269
|
-
setTimeout(() => resolve(), 5000); // 5
|
|
1269
|
+
setTimeout(() => resolve(), 5000); // timeout after 5 seconds
|
|
1270
1270
|
});
|
|
1271
1271
|
this.processes.delete(processId);
|
|
1272
1272
|
return {
|
|
@@ -1287,7 +1287,7 @@ export class ProcessManager {
|
|
|
1287
1287
|
listProcesses() {
|
|
1288
1288
|
const processes = [];
|
|
1289
1289
|
for (const [pid] of this.processes) {
|
|
1290
|
-
//
|
|
1290
|
+
// Find corresponding execution info
|
|
1291
1291
|
const execution = Array.from(this.executions.values()).find((exec) => exec.process_id === pid);
|
|
1292
1292
|
if (execution) {
|
|
1293
1293
|
const processInfo = {
|
|
@@ -1315,7 +1315,7 @@ export class ProcessManager {
|
|
|
1315
1315
|
return processes;
|
|
1316
1316
|
}
|
|
1317
1317
|
cleanup() {
|
|
1318
|
-
//
|
|
1318
|
+
// Terminate all running processes
|
|
1319
1319
|
for (const [, childProcess] of this.processes) {
|
|
1320
1320
|
try {
|
|
1321
1321
|
childProcess.kill('SIGTERM');
|
|
@@ -1326,17 +1326,17 @@ export class ProcessManager {
|
|
|
1326
1326
|
}, 5000);
|
|
1327
1327
|
}
|
|
1328
1328
|
catch (error) {
|
|
1329
|
-
//
|
|
1329
|
+
// Record error in internal log (avoid stdout)
|
|
1330
1330
|
// console.error(`Failed to cleanup process ${pid}:`, error);
|
|
1331
1331
|
}
|
|
1332
1332
|
}
|
|
1333
1333
|
this.processes.clear();
|
|
1334
1334
|
this.executions.clear();
|
|
1335
1335
|
}
|
|
1336
|
-
//
|
|
1336
|
+
// Working directory management
|
|
1337
1337
|
setDefaultWorkingDirectory(workingDirectory) {
|
|
1338
1338
|
const previousWorkdir = this.defaultWorkingDirectory;
|
|
1339
|
-
//
|
|
1339
|
+
// Validate directory
|
|
1340
1340
|
if (!this.isAllowedWorkingDirectory(workingDirectory)) {
|
|
1341
1341
|
throw new Error(`Working directory not allowed: ${workingDirectory}`);
|
|
1342
1342
|
}
|
|
@@ -1355,7 +1355,7 @@ export class ProcessManager {
|
|
|
1355
1355
|
return [...this.allowedWorkingDirectories];
|
|
1356
1356
|
}
|
|
1357
1357
|
isAllowedWorkingDirectory(workingDirectory) {
|
|
1358
|
-
//
|
|
1358
|
+
// Compare using normalized paths
|
|
1359
1359
|
const normalizedPath = path.resolve(workingDirectory);
|
|
1360
1360
|
return this.allowedWorkingDirectories.some((allowedDir) => {
|
|
1361
1361
|
const normalizedAllowed = path.resolve(allowedDir);
|