@mako10k/shell-server 0.2.4 → 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.
- package/README.md +48 -1
- package/dist/backoffice/index.js +1 -1
- package/dist/backoffice/index.js.map +1 -1
- package/dist/cli.js +25 -4
- package/dist/cli.js.map +1 -1
- package/dist/core/process-manager.d.ts +4 -1
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +299 -186
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/server-manager.d.ts +1 -0
- package/dist/core/server-manager.d.ts.map +1 -1
- package/dist/core/server-manager.js +26 -0
- package/dist/core/server-manager.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +165 -1
- package/dist/daemon/server.js.map +1 -1
- package/dist/runtime/tool-runtime.d.ts.map +1 -1
- package/dist/runtime/tool-runtime.js +9 -2
- package/dist/runtime/tool-runtime.js.map +1 -1
- package/dist/security/chat-completion-adapter.d.ts +2 -0
- package/dist/security/chat-completion-adapter.d.ts.map +1 -1
- package/dist/security/chat-completion-adapter.js +121 -4
- package/dist/security/chat-completion-adapter.js.map +1 -1
- package/dist/security/enhanced-evaluator.d.ts +2 -0
- package/dist/security/enhanced-evaluator.d.ts.map +1 -1
- package/dist/security/enhanced-evaluator.js +177 -8
- package/dist/security/enhanced-evaluator.js.map +1 -1
- package/dist/security/evaluator-types.d.ts +2 -2
- package/dist/security/kfence-fastpath.d.ts +15 -0
- package/dist/security/kfence-fastpath.d.ts.map +1 -0
- package/dist/security/kfence-fastpath.js +20 -0
- package/dist/security/kfence-fastpath.js.map +1 -0
- package/dist/security/manager.d.ts +4 -0
- package/dist/security/manager.d.ts.map +1 -1
- package/dist/security/manager.js +6 -0
- package/dist/security/manager.js.map +1 -1
- package/dist/security/security-llm-prompt-generator.js +1 -1
- 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 +32 -14
- package/dist/tools/shell-tools.js.map +1 -1
- package/dist/types/enhanced-security.d.ts +6 -6
- package/dist/types/index.d.ts +18 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +16 -13
- package/dist/types/index.js.map +1 -1
- package/dist/types/schemas.d.ts +39 -58
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +22 -27
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +2 -2
- package/dist/utils/errors.js.map +1 -1
- package/package.json +1 -1
|
@@ -22,8 +22,68 @@ export class ProcessManager {
|
|
|
22
22
|
fileStorageSubscriber;
|
|
23
23
|
realtimeStreamSubscriber;
|
|
24
24
|
enableStreaming = false; // Feature Flag
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
buildConcurrencyStopCandidates(limit = 5) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
return Array.from(this.executions.values())
|
|
28
|
+
.filter((exec) => exec.status === 'running')
|
|
29
|
+
.sort((a, b) => {
|
|
30
|
+
const aStart = a.started_at ? new Date(a.started_at).getTime() : Number.MAX_SAFE_INTEGER;
|
|
31
|
+
const bStart = b.started_at ? new Date(b.started_at).getTime() : Number.MAX_SAFE_INTEGER;
|
|
32
|
+
return aStart - bStart;
|
|
33
|
+
})
|
|
34
|
+
.slice(0, limit)
|
|
35
|
+
.map((exec) => {
|
|
36
|
+
const startedAtMs = exec.started_at ? new Date(exec.started_at).getTime() : undefined;
|
|
37
|
+
const runtimeSeconds = startedAtMs && Number.isFinite(startedAtMs)
|
|
38
|
+
? Math.max(0, Math.floor((now - startedAtMs) / 1000))
|
|
39
|
+
: undefined;
|
|
40
|
+
return {
|
|
41
|
+
execution_id: exec.execution_id,
|
|
42
|
+
...(exec.process_id !== undefined ? { process_id: exec.process_id } : {}),
|
|
43
|
+
command: exec.command.length > 120 ? `${exec.command.slice(0, 117)}...` : exec.command,
|
|
44
|
+
...(runtimeSeconds !== undefined ? { runtime_seconds: runtimeSeconds } : {}),
|
|
45
|
+
status: exec.status,
|
|
46
|
+
suggested_action: {
|
|
47
|
+
tool: 'process_kill',
|
|
48
|
+
parameters: {
|
|
49
|
+
...(exec.process_id !== undefined ? { process_id: exec.process_id } : {}),
|
|
50
|
+
signal: 'TERM',
|
|
51
|
+
force: false,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
getRunningExecutionCount() {
|
|
58
|
+
return Array.from(this.executions.values()).filter((exec) => exec.status === 'running').length;
|
|
59
|
+
}
|
|
60
|
+
async waitForExecutionSlot(maxWaitMs, pollIntervalMs = 100) {
|
|
61
|
+
if (this.getRunningExecutionCount() < this.maxConcurrentProcesses) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (maxWaitMs <= 0) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const deadline = Date.now() + maxWaitMs;
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
await new Promise((resolve) => {
|
|
70
|
+
setTimeout(resolve, Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
71
|
+
});
|
|
72
|
+
if (this.getRunningExecutionCount() < this.maxConcurrentProcesses) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return this.getRunningExecutionCount() < this.maxConcurrentProcesses;
|
|
77
|
+
}
|
|
78
|
+
constructor(maxConcurrentProcesses = 8, outputDir = '/tmp/mcp-shell-outputs', fileManager) {
|
|
79
|
+
const envMaxConcurrentRaw = process.env['SHELL_SERVER_MAX_CONCURRENT_PROCESSES'];
|
|
80
|
+
const envMaxConcurrentParsed = envMaxConcurrentRaw
|
|
81
|
+
? Number.parseInt(envMaxConcurrentRaw, 10)
|
|
82
|
+
: NaN;
|
|
83
|
+
const resolvedMaxConcurrent = Number.isFinite(envMaxConcurrentParsed) && envMaxConcurrentParsed > 0
|
|
84
|
+
? envMaxConcurrentParsed
|
|
85
|
+
: maxConcurrentProcesses;
|
|
86
|
+
this.maxConcurrentProcesses = resolvedMaxConcurrent;
|
|
27
87
|
this.outputDir = outputDir;
|
|
28
88
|
this.fileManager = fileManager;
|
|
29
89
|
this.defaultWorkingDirectory = process.env['SHELL_SERVER_DEFAULT_WORKDIR'] || process.cwd();
|
|
@@ -111,41 +171,70 @@ export class ProcessManager {
|
|
|
111
171
|
await ensureDirectory(this.outputDir);
|
|
112
172
|
}
|
|
113
173
|
async executeCommand(options) {
|
|
114
|
-
|
|
115
|
-
|
|
174
|
+
let effectiveOptions = { ...options };
|
|
175
|
+
// For adaptive mode, when all slots are occupied, wait for a free slot
|
|
176
|
+
// using the same foreground wait budget requested by the caller.
|
|
177
|
+
const runningProcesses = this.getRunningExecutionCount();
|
|
116
178
|
if (runningProcesses >= this.maxConcurrentProcesses) {
|
|
117
|
-
|
|
179
|
+
const queueWaitBudgetSeconds = effectiveOptions.executionMode === 'adaptive'
|
|
180
|
+
? (effectiveOptions.foregroundTimeoutSeconds ?? 10)
|
|
181
|
+
: 0;
|
|
182
|
+
const queueWaitBudgetMs = Math.max(0, Math.floor(queueWaitBudgetSeconds * 1000));
|
|
183
|
+
const waitStartedAt = Date.now();
|
|
184
|
+
const acquired = await this.waitForExecutionSlot(queueWaitBudgetMs);
|
|
185
|
+
const waitedMs = Date.now() - waitStartedAt;
|
|
186
|
+
if (!acquired) {
|
|
187
|
+
const currentRunning = this.getRunningExecutionCount();
|
|
188
|
+
const stopCandidates = this.buildConcurrencyStopCandidates();
|
|
189
|
+
throw new ResourceLimitError('concurrent processes', this.maxConcurrentProcesses, undefined, {
|
|
190
|
+
code: 'CONCURRENCY_LIMIT_EXCEEDED',
|
|
191
|
+
reason: `Concurrent execution limit reached (${currentRunning}/${this.maxConcurrentProcesses}) and no slot became available within ${Math.floor(waitedMs / 1000)}s queue wait budget.`,
|
|
192
|
+
running_count: currentRunning,
|
|
193
|
+
limit: this.maxConcurrentProcesses,
|
|
194
|
+
queue_wait_budget_seconds: queueWaitBudgetSeconds,
|
|
195
|
+
waited_seconds: Math.floor(waitedMs / 1000),
|
|
196
|
+
stop_candidates: stopCandidates,
|
|
197
|
+
next_steps: [
|
|
198
|
+
'Call process_list with status_filter="running" to inspect active executions.',
|
|
199
|
+
'Stop one or more long-running commands via process_kill, then retry shell_execute.',
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (effectiveOptions.executionMode === 'adaptive') {
|
|
204
|
+
const originalForegroundWaitSeconds = effectiveOptions.foregroundTimeoutSeconds ?? 10;
|
|
205
|
+
const remainingForegroundWaitSeconds = Math.max(1, Math.floor((originalForegroundWaitSeconds * 1000 - waitedMs) / 1000));
|
|
206
|
+
effectiveOptions = {
|
|
207
|
+
...effectiveOptions,
|
|
208
|
+
foregroundTimeoutSeconds: remainingForegroundWaitSeconds,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
118
211
|
}
|
|
119
212
|
// Prepare input data when input_output_id is specified
|
|
120
|
-
let resolvedInputData =
|
|
213
|
+
let resolvedInputData = effectiveOptions.inputData;
|
|
121
214
|
let inputStream = undefined;
|
|
122
|
-
if (
|
|
215
|
+
if (effectiveOptions.inputOutputId) {
|
|
123
216
|
if (!this.fileManager) {
|
|
124
217
|
throw new ExecutionError('FileManager is not available for input_output_id processing', {
|
|
125
|
-
inputOutputId:
|
|
218
|
+
inputOutputId: effectiveOptions.inputOutputId,
|
|
126
219
|
});
|
|
127
220
|
}
|
|
128
|
-
|
|
129
|
-
const sourceExecutionId = this.findExecutionIdByOutputId(options.inputOutputId);
|
|
221
|
+
const sourceExecutionId = this.findExecutionIdByOutputId(effectiveOptions.inputOutputId);
|
|
130
222
|
if (sourceExecutionId && this.realtimeStreamSubscriber) {
|
|
131
|
-
// For active processes: use StreamingPipelineReader
|
|
132
223
|
const streamState = this.realtimeStreamSubscriber.getStreamState(sourceExecutionId);
|
|
133
224
|
if (streamState && streamState.isActive) {
|
|
134
225
|
console.error(`ProcessManager: Using streaming pipeline for active process ${sourceExecutionId}`);
|
|
135
|
-
inputStream = new StreamingPipelineReader(this.fileManager, this.realtimeStreamSubscriber,
|
|
226
|
+
inputStream = new StreamingPipelineReader(this.fileManager, this.realtimeStreamSubscriber, effectiveOptions.inputOutputId, sourceExecutionId);
|
|
136
227
|
}
|
|
137
228
|
}
|
|
138
|
-
// If not active (or on failure), fall back to traditional file read
|
|
139
229
|
if (!inputStream) {
|
|
140
230
|
try {
|
|
141
|
-
console.error(`ProcessManager: Using traditional file read for ${
|
|
142
|
-
const result = await this.fileManager.readFile(
|
|
143
|
-
'utf-8');
|
|
231
|
+
console.error(`ProcessManager: Using traditional file read for ${effectiveOptions.inputOutputId}`);
|
|
232
|
+
const result = await this.fileManager.readFile(effectiveOptions.inputOutputId, 0, 100 * 1024 * 1024, 'utf-8');
|
|
144
233
|
resolvedInputData = result.content;
|
|
145
234
|
}
|
|
146
235
|
catch (error) {
|
|
147
|
-
throw new ExecutionError(`Failed to read input from output_id: ${
|
|
148
|
-
inputOutputId:
|
|
236
|
+
throw new ExecutionError(`Failed to read input from output_id: ${effectiveOptions.inputOutputId}`, {
|
|
237
|
+
inputOutputId: effectiveOptions.inputOutputId,
|
|
149
238
|
originalError: String(error),
|
|
150
239
|
});
|
|
151
240
|
}
|
|
@@ -153,11 +242,10 @@ export class ProcessManager {
|
|
|
153
242
|
}
|
|
154
243
|
const executionId = generateId();
|
|
155
244
|
const startTime = getCurrentTimestamp();
|
|
156
|
-
|
|
157
|
-
const resolvedWorkingDirectory = this.resolveWorkingDirectory(options.workingDirectory);
|
|
245
|
+
const resolvedWorkingDirectory = this.resolveWorkingDirectory(effectiveOptions.workingDirectory);
|
|
158
246
|
const executionInfo = {
|
|
159
247
|
execution_id: executionId,
|
|
160
|
-
command:
|
|
248
|
+
command: effectiveOptions.command,
|
|
161
249
|
status: 'running',
|
|
162
250
|
working_directory: resolvedWorkingDirectory,
|
|
163
251
|
default_working_directory: this.defaultWorkingDirectory,
|
|
@@ -165,30 +253,27 @@ export class ProcessManager {
|
|
|
165
253
|
created_at: startTime,
|
|
166
254
|
started_at: startTime,
|
|
167
255
|
};
|
|
168
|
-
if (
|
|
169
|
-
executionInfo.environment_variables =
|
|
256
|
+
if (effectiveOptions.environmentVariables) {
|
|
257
|
+
executionInfo.environment_variables = effectiveOptions.environmentVariables;
|
|
170
258
|
}
|
|
171
259
|
this.executions.set(executionId, executionInfo);
|
|
172
|
-
|
|
173
|
-
if (options.createTerminal && this.terminalManager) {
|
|
260
|
+
if (effectiveOptions.createTerminal && this.terminalManager) {
|
|
174
261
|
try {
|
|
175
262
|
const terminalOptions = {
|
|
176
263
|
sessionName: `exec-${executionId}`,
|
|
177
|
-
shellType:
|
|
178
|
-
dimensions:
|
|
264
|
+
shellType: effectiveOptions.terminalShell || 'bash',
|
|
265
|
+
dimensions: effectiveOptions.terminalDimensions || { width: 80, height: 24 },
|
|
179
266
|
autoSaveHistory: true,
|
|
180
267
|
};
|
|
181
|
-
if (
|
|
182
|
-
terminalOptions.workingDirectory =
|
|
268
|
+
if (effectiveOptions.workingDirectory) {
|
|
269
|
+
terminalOptions.workingDirectory = effectiveOptions.workingDirectory;
|
|
183
270
|
}
|
|
184
|
-
if (
|
|
185
|
-
terminalOptions.environmentVariables =
|
|
271
|
+
if (effectiveOptions.environmentVariables) {
|
|
272
|
+
terminalOptions.environmentVariables = effectiveOptions.environmentVariables;
|
|
186
273
|
}
|
|
187
274
|
const terminalInfo = await this.terminalManager.createTerminal(terminalOptions);
|
|
188
275
|
executionInfo.terminal_id = terminalInfo.terminal_id;
|
|
189
|
-
|
|
190
|
-
this.terminalManager.sendInput(terminalInfo.terminal_id, options.command, true);
|
|
191
|
-
// Update execution info
|
|
276
|
+
this.terminalManager.sendInput(terminalInfo.terminal_id, effectiveOptions.command, true);
|
|
192
277
|
executionInfo.status = 'completed';
|
|
193
278
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
194
279
|
this.executions.set(executionId, executionInfo);
|
|
@@ -204,17 +289,15 @@ export class ProcessManager {
|
|
|
204
289
|
}
|
|
205
290
|
}
|
|
206
291
|
try {
|
|
207
|
-
|
|
208
|
-
const { inputOutputId: _inputOutputId, ...baseOptions } = options;
|
|
292
|
+
const { inputOutputId: _inputOutputId, ...baseOptions } = effectiveOptions;
|
|
209
293
|
const updatedOptions = {
|
|
210
294
|
...baseOptions,
|
|
211
295
|
...(resolvedInputData !== undefined && { inputData: resolvedInputData }),
|
|
212
296
|
};
|
|
213
|
-
// Special handling when StreamingPipelineReader exists
|
|
214
297
|
if (inputStream) {
|
|
215
298
|
return await this.executeCommandWithInputStream(executionId, updatedOptions, inputStream);
|
|
216
299
|
}
|
|
217
|
-
switch (
|
|
300
|
+
switch (effectiveOptions.executionMode) {
|
|
218
301
|
case 'foreground':
|
|
219
302
|
return await this.executeForegroundCommand(executionId, updatedOptions);
|
|
220
303
|
case 'adaptive':
|
|
@@ -224,11 +307,10 @@ export class ProcessManager {
|
|
|
224
307
|
case 'detached':
|
|
225
308
|
return await this.executeDetachedCommand(executionId, updatedOptions);
|
|
226
309
|
default:
|
|
227
|
-
throw new ExecutionError('Unsupported execution mode', { mode:
|
|
310
|
+
throw new ExecutionError('Unsupported execution mode', { mode: effectiveOptions.executionMode });
|
|
228
311
|
}
|
|
229
312
|
}
|
|
230
313
|
catch (error) {
|
|
231
|
-
// Update execution info on error
|
|
232
314
|
const updatedInfo = this.executions.get(executionId);
|
|
233
315
|
if (updatedInfo) {
|
|
234
316
|
updatedInfo.status = 'failed';
|
|
@@ -347,17 +429,21 @@ export class ProcessManager {
|
|
|
347
429
|
reject(new ExecutionError(`Process error: ${error.message}`, { originalError: String(error) }));
|
|
348
430
|
});
|
|
349
431
|
// Timeout handling
|
|
350
|
-
const timeout =
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
child.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
432
|
+
const timeout = options.timeoutSeconds !== undefined
|
|
433
|
+
? setTimeout(() => {
|
|
434
|
+
console.error(`Process timeout for ${executionId}`);
|
|
435
|
+
child.kill('SIGTERM');
|
|
436
|
+
setTimeout(() => {
|
|
437
|
+
if (!child.killed) {
|
|
438
|
+
child.kill('SIGKILL');
|
|
439
|
+
}
|
|
440
|
+
}, 5000);
|
|
441
|
+
}, options.timeoutSeconds * 1000)
|
|
442
|
+
: undefined;
|
|
359
443
|
child.on('close', () => {
|
|
360
|
-
|
|
444
|
+
if (timeout) {
|
|
445
|
+
clearTimeout(timeout);
|
|
446
|
+
}
|
|
361
447
|
});
|
|
362
448
|
});
|
|
363
449
|
}
|
|
@@ -379,46 +465,48 @@ export class ProcessManager {
|
|
|
379
465
|
this.processes.set(childProcess.pid, childProcess);
|
|
380
466
|
}
|
|
381
467
|
// Set timeout
|
|
382
|
-
const timeout =
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
childProcess.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
executionInfo
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
468
|
+
const timeout = options.timeoutSeconds !== undefined
|
|
469
|
+
? setTimeout(async () => {
|
|
470
|
+
childProcess.kill('SIGTERM');
|
|
471
|
+
setTimeout(() => {
|
|
472
|
+
if (!childProcess.killed) {
|
|
473
|
+
childProcess.kill('SIGKILL');
|
|
474
|
+
}
|
|
475
|
+
}, 5000);
|
|
476
|
+
const executionTime = Date.now() - startTime;
|
|
477
|
+
const executionInfo = this.executions.get(executionId);
|
|
478
|
+
if (executionInfo) {
|
|
479
|
+
executionInfo.status = 'timeout';
|
|
480
|
+
executionInfo.stdout = sanitizeString(stdout);
|
|
481
|
+
executionInfo.stderr = sanitizeString(stderr);
|
|
482
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
483
|
+
executionInfo.execution_time_ms = executionTime;
|
|
484
|
+
if (childProcess.pid !== undefined) {
|
|
485
|
+
executionInfo.process_id = childProcess.pid;
|
|
486
|
+
}
|
|
487
|
+
// Save output to FileManager (regardless of size)
|
|
488
|
+
let outputFileId;
|
|
489
|
+
try {
|
|
490
|
+
outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
491
|
+
executionInfo.output_id = outputFileId;
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
495
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
496
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
497
|
+
}
|
|
498
|
+
// Set detailed output status
|
|
499
|
+
this.setOutputStatus(executionInfo, outputTruncated, 'timeout', outputFileId);
|
|
500
|
+
this.executions.set(executionId, executionInfo);
|
|
501
|
+
// Return partial result when return_partial_on_timeout is true
|
|
502
|
+
if (options.returnPartialOnTimeout) {
|
|
503
|
+
resolve(executionInfo);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
418
506
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
507
|
+
reject(new TimeoutError(options.timeoutSeconds ?? 0));
|
|
508
|
+
}, options.timeoutSeconds * 1000)
|
|
509
|
+
: undefined;
|
|
422
510
|
// Send stdin
|
|
423
511
|
if (options.inputData) {
|
|
424
512
|
childProcess.stdin?.write(options.inputData);
|
|
@@ -453,7 +541,9 @@ export class ProcessManager {
|
|
|
453
541
|
}
|
|
454
542
|
// Handle process close
|
|
455
543
|
childProcess.on('close', async (code) => {
|
|
456
|
-
|
|
544
|
+
if (timeout) {
|
|
545
|
+
clearTimeout(timeout);
|
|
546
|
+
}
|
|
457
547
|
if (childProcess.pid) {
|
|
458
548
|
this.processes.delete(childProcess.pid);
|
|
459
549
|
}
|
|
@@ -542,39 +632,41 @@ export class ProcessManager {
|
|
|
542
632
|
}
|
|
543
633
|
}, foregroundTimeout * 1000);
|
|
544
634
|
// Set final timeout
|
|
545
|
-
const finalTimeoutHandle =
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
childProcess.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
executionInfo
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
635
|
+
const finalTimeoutHandle = options.timeoutSeconds !== undefined
|
|
636
|
+
? setTimeout(async () => {
|
|
637
|
+
childProcess.kill('SIGTERM');
|
|
638
|
+
setTimeout(() => {
|
|
639
|
+
if (!childProcess.killed) {
|
|
640
|
+
childProcess.kill('SIGKILL');
|
|
641
|
+
}
|
|
642
|
+
}, 5000);
|
|
643
|
+
const executionInfo = this.executions.get(executionId);
|
|
644
|
+
if (executionInfo) {
|
|
645
|
+
executionInfo.status = 'timeout';
|
|
646
|
+
executionInfo.stdout = sanitizeString(stdout);
|
|
647
|
+
executionInfo.stderr = sanitizeString(stderr);
|
|
648
|
+
executionInfo.output_truncated = outputTruncated;
|
|
649
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
650
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
651
|
+
// Save output to FileManager
|
|
652
|
+
try {
|
|
653
|
+
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
654
|
+
executionInfo.output_id = outputFileId;
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
658
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
659
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
660
|
+
}
|
|
661
|
+
this.executions.set(executionId, executionInfo);
|
|
662
|
+
if (returnPartialOnTimeout) {
|
|
663
|
+
resolve(executionInfo);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
574
666
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
667
|
+
reject(new TimeoutError(options.timeoutSeconds ?? 0));
|
|
668
|
+
}, options.timeoutSeconds * 1000)
|
|
669
|
+
: undefined;
|
|
578
670
|
// Function to transition to background mode
|
|
579
671
|
const transitionToBackground = async () => {
|
|
580
672
|
clearTimeout(foregroundTimeoutHandle);
|
|
@@ -608,9 +700,14 @@ export class ProcessManager {
|
|
|
608
700
|
this.setOutputStatus(executionInfo, outputTruncated, 'background_transition', outputFileId);
|
|
609
701
|
this.executions.set(executionId, executionInfo);
|
|
610
702
|
// Configure continued background handling (adaptive mode only)
|
|
703
|
+
const remainingTimeoutSeconds = options.timeoutSeconds !== undefined
|
|
704
|
+
? Math.max(1, options.timeoutSeconds - Math.floor((Date.now() - startTime) / 1000))
|
|
705
|
+
: undefined;
|
|
611
706
|
this.handleAdaptiveBackgroundTransition(executionId, childProcess, {
|
|
612
707
|
...options,
|
|
613
|
-
|
|
708
|
+
...(remainingTimeoutSeconds !== undefined
|
|
709
|
+
? { timeoutSeconds: remainingTimeoutSeconds }
|
|
710
|
+
: {}),
|
|
614
711
|
});
|
|
615
712
|
resolve(executionInfo);
|
|
616
713
|
}
|
|
@@ -660,7 +757,9 @@ export class ProcessManager {
|
|
|
660
757
|
// Handle process close
|
|
661
758
|
childProcess.on('close', async (code) => {
|
|
662
759
|
clearTimeout(foregroundTimeoutHandle);
|
|
663
|
-
|
|
760
|
+
if (finalTimeoutHandle) {
|
|
761
|
+
clearTimeout(finalTimeoutHandle);
|
|
762
|
+
}
|
|
664
763
|
if (childProcess.pid) {
|
|
665
764
|
this.processes.delete(childProcess.pid);
|
|
666
765
|
}
|
|
@@ -697,7 +796,9 @@ export class ProcessManager {
|
|
|
697
796
|
// Error handling
|
|
698
797
|
childProcess.on('error', (error) => {
|
|
699
798
|
clearTimeout(foregroundTimeoutHandle);
|
|
700
|
-
|
|
799
|
+
if (finalTimeoutHandle) {
|
|
800
|
+
clearTimeout(finalTimeoutHandle);
|
|
801
|
+
}
|
|
701
802
|
if (childProcess.pid) {
|
|
702
803
|
this.processes.delete(childProcess.pid);
|
|
703
804
|
}
|
|
@@ -747,52 +848,54 @@ export class ProcessManager {
|
|
|
747
848
|
let stdout = '';
|
|
748
849
|
let stderr = '';
|
|
749
850
|
// Set timeout (for background processes)
|
|
750
|
-
const timeout =
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
childProcess.
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
executionInfo
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
851
|
+
const timeout = options.timeoutSeconds !== undefined
|
|
852
|
+
? setTimeout(async () => {
|
|
853
|
+
childProcess.kill('SIGTERM');
|
|
854
|
+
setTimeout(() => {
|
|
855
|
+
if (!childProcess.killed) {
|
|
856
|
+
childProcess.kill('SIGKILL');
|
|
857
|
+
}
|
|
858
|
+
}, 5000);
|
|
859
|
+
const executionInfo = this.executions.get(executionId);
|
|
860
|
+
if (executionInfo) {
|
|
861
|
+
executionInfo.status = 'timeout';
|
|
862
|
+
executionInfo.stdout = stdout;
|
|
863
|
+
executionInfo.stderr = stderr;
|
|
864
|
+
executionInfo.output_truncated = true;
|
|
865
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
866
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
867
|
+
// Save output to FileManager
|
|
868
|
+
try {
|
|
869
|
+
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
870
|
+
executionInfo.output_id = outputFileId;
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
874
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
875
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
876
|
+
}
|
|
877
|
+
this.executions.set(executionId, executionInfo);
|
|
878
|
+
// Invoke timeout callback for background process
|
|
879
|
+
if (this.backgroundProcessCallbacks.onTimeout) {
|
|
880
|
+
setImmediate(async () => {
|
|
881
|
+
try {
|
|
882
|
+
const callback = this.backgroundProcessCallbacks.onTimeout;
|
|
883
|
+
if (callback) {
|
|
884
|
+
const result = callback(executionId, executionInfo);
|
|
885
|
+
if (result instanceof Promise) {
|
|
886
|
+
await result;
|
|
887
|
+
}
|
|
785
888
|
}
|
|
786
889
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
}
|
|
792
|
-
}
|
|
890
|
+
catch (callbackError) {
|
|
891
|
+
// Record callback errors in internal logs only
|
|
892
|
+
// console.error('Background process timeout callback error:', callbackError);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
793
896
|
}
|
|
794
|
-
}
|
|
795
|
-
|
|
897
|
+
}, options.timeoutSeconds * 1000)
|
|
898
|
+
: undefined;
|
|
796
899
|
// Collect output
|
|
797
900
|
childProcess.stdout?.on('data', (data) => {
|
|
798
901
|
stdout += data.toString();
|
|
@@ -804,7 +907,9 @@ export class ProcessManager {
|
|
|
804
907
|
}
|
|
805
908
|
// Handle process close
|
|
806
909
|
childProcess.on('close', async (code) => {
|
|
807
|
-
|
|
910
|
+
if (timeout) {
|
|
911
|
+
clearTimeout(timeout);
|
|
912
|
+
}
|
|
808
913
|
if (childProcess.pid) {
|
|
809
914
|
this.processes.delete(childProcess.pid);
|
|
810
915
|
}
|
|
@@ -846,7 +951,9 @@ export class ProcessManager {
|
|
|
846
951
|
}
|
|
847
952
|
});
|
|
848
953
|
childProcess.on('error', (error) => {
|
|
849
|
-
|
|
954
|
+
if (timeout) {
|
|
955
|
+
clearTimeout(timeout);
|
|
956
|
+
}
|
|
850
957
|
if (childProcess.pid) {
|
|
851
958
|
this.processes.delete(childProcess.pid);
|
|
852
959
|
}
|
|
@@ -880,24 +987,28 @@ export class ProcessManager {
|
|
|
880
987
|
// Handle processes transitioned to background in adaptive mode
|
|
881
988
|
handleAdaptiveBackgroundTransition(executionId, childProcess, options) {
|
|
882
989
|
// Set timeout (final timeout)
|
|
883
|
-
const timeout =
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
childProcess.
|
|
990
|
+
const timeout = options.timeoutSeconds !== undefined
|
|
991
|
+
? setTimeout(async () => {
|
|
992
|
+
childProcess.kill('SIGTERM');
|
|
993
|
+
setTimeout(() => {
|
|
994
|
+
if (!childProcess.killed) {
|
|
995
|
+
childProcess.kill('SIGKILL');
|
|
996
|
+
}
|
|
997
|
+
}, 5000);
|
|
998
|
+
const executionInfo = this.executions.get(executionId);
|
|
999
|
+
if (executionInfo) {
|
|
1000
|
+
executionInfo.status = 'timeout';
|
|
1001
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
1002
|
+
// Keep existing output (already captured in adaptive mode)
|
|
1003
|
+
this.executions.set(executionId, executionInfo);
|
|
888
1004
|
}
|
|
889
|
-
},
|
|
890
|
-
|
|
891
|
-
if (executionInfo) {
|
|
892
|
-
executionInfo.status = 'timeout';
|
|
893
|
-
executionInfo.completed_at = getCurrentTimestamp();
|
|
894
|
-
// Keep existing output (already captured in adaptive mode)
|
|
895
|
-
this.executions.set(executionId, executionInfo);
|
|
896
|
-
}
|
|
897
|
-
}, options.timeoutSeconds * 1000);
|
|
1005
|
+
}, options.timeoutSeconds * 1000)
|
|
1006
|
+
: undefined;
|
|
898
1007
|
// Handle process close
|
|
899
1008
|
childProcess.on('close', async (code) => {
|
|
900
|
-
|
|
1009
|
+
if (timeout) {
|
|
1010
|
+
clearTimeout(timeout);
|
|
1011
|
+
}
|
|
901
1012
|
if (childProcess.pid) {
|
|
902
1013
|
this.processes.delete(childProcess.pid);
|
|
903
1014
|
}
|
|
@@ -933,7 +1044,9 @@ export class ProcessManager {
|
|
|
933
1044
|
}
|
|
934
1045
|
});
|
|
935
1046
|
childProcess.on('error', (error) => {
|
|
936
|
-
|
|
1047
|
+
if (timeout) {
|
|
1048
|
+
clearTimeout(timeout);
|
|
1049
|
+
}
|
|
937
1050
|
if (childProcess.pid) {
|
|
938
1051
|
this.processes.delete(childProcess.pid);
|
|
939
1052
|
}
|