@mako10k/shell-server 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.
- package/README.md +83 -11
- package/dist/backoffice/index.js +1 -1
- package/dist/backoffice/index.js.map +1 -1
- package/dist/backoffice/server.js +9 -9
- package/dist/backoffice/server.js.map +1 -1
- package/dist/cli.js +25 -4
- package/dist/cli.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 +8 -5
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +427 -314
- 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/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/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/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +165 -1
- package/dist/daemon/server.js.map +1 -1
- package/dist/executor/server.js +3 -3
- package/dist/executor/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 +124 -7
- 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 +180 -11
- 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 +8 -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 +5 -5
- 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 +59 -41
- package/dist/tools/shell-tools.js.map +1 -1
- package/dist/types/enhanced-security.d.ts +6 -6
- package/dist/types/enhanced-security.js +29 -29
- package/dist/types/enhanced-security.js.map +1 -1
- package/dist/types/index.d.ts +18 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +37 -34
- 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.d.ts +39 -58
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +25 -30
- 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 +3 -3
- 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 +1 -1
|
@@ -12,63 +12,123 @@ 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;
|
|
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();
|
|
30
90
|
this.allowedWorkingDirectories = process.env['SHELL_SERVER_ALLOWED_WORKDIRS']
|
|
31
91
|
? process.env['SHELL_SERVER_ALLOWED_WORKDIRS'].split(',').map((dir) => dir.trim())
|
|
32
92
|
: [process.cwd()];
|
|
33
|
-
// StreamPublisher
|
|
93
|
+
// Initialize StreamPublisher
|
|
34
94
|
this.streamPublisher = new StreamPublisher({
|
|
35
|
-
enableRealtimeStreaming: false, //
|
|
95
|
+
enableRealtimeStreaming: false, // disabled by default
|
|
36
96
|
bufferSize: 8192,
|
|
37
97
|
notificationInterval: 100,
|
|
38
98
|
});
|
|
39
|
-
//
|
|
99
|
+
// Control streaming via environment variable (phased rollout, enabled by default)
|
|
40
100
|
this.enableStreaming = process.env['SHELL_SERVER_ENABLE_STREAMING'] !== 'false';
|
|
41
101
|
if (this.enableStreaming) {
|
|
42
102
|
this.initializeStreamingComponents();
|
|
43
103
|
}
|
|
44
104
|
this.initializeOutputDirectory();
|
|
45
105
|
}
|
|
46
|
-
// TerminalManager
|
|
106
|
+
// Set TerminalManager reference
|
|
47
107
|
setTerminalManager(terminalManager) {
|
|
48
108
|
this.terminalManager = terminalManager;
|
|
49
109
|
}
|
|
50
|
-
// FileManager
|
|
110
|
+
// Set FileManager reference
|
|
51
111
|
setFileManager(fileManager) {
|
|
52
112
|
this.fileManager = fileManager;
|
|
53
|
-
// FileManager
|
|
113
|
+
// Reinitialize streaming when FileManager is set
|
|
54
114
|
if (this.enableStreaming) {
|
|
55
115
|
this.initializeStreamingComponents();
|
|
56
116
|
}
|
|
57
117
|
}
|
|
58
|
-
//
|
|
118
|
+
// Set callbacks for background process completion
|
|
59
119
|
setBackgroundProcessCallbacks(callbacks) {
|
|
60
120
|
this.backgroundProcessCallbacks = callbacks;
|
|
61
121
|
}
|
|
62
|
-
// Issue #13:
|
|
122
|
+
// Issue #13: Initialize streaming components
|
|
63
123
|
initializeStreamingComponents() {
|
|
64
124
|
if (!this.fileManager) {
|
|
65
125
|
console.error('ProcessManager: FileManager is required for streaming components');
|
|
66
126
|
return;
|
|
67
127
|
}
|
|
68
|
-
// FileStorageSubscriber
|
|
128
|
+
// Initialize FileStorageSubscriber (replacing part of existing FileManager handling)
|
|
69
129
|
this.fileStorageSubscriber = new FileStorageSubscriber(this.fileManager, this.outputDir);
|
|
70
130
|
this.streamPublisher.subscribe(this.fileStorageSubscriber);
|
|
71
|
-
// RealtimeStreamSubscriber
|
|
131
|
+
// Initialize RealtimeStreamSubscriber
|
|
72
132
|
this.realtimeStreamSubscriber = new RealtimeStreamSubscriber({
|
|
73
133
|
bufferSize: 8192,
|
|
74
134
|
notificationInterval: 100,
|
|
@@ -78,14 +138,14 @@ export class ProcessManager {
|
|
|
78
138
|
this.streamPublisher.subscribe(this.realtimeStreamSubscriber);
|
|
79
139
|
console.error('ProcessManager: Streaming components initialized');
|
|
80
140
|
}
|
|
81
|
-
// Issue #13:
|
|
141
|
+
// Issue #13: Enable/disable streaming
|
|
82
142
|
enableStreamingFeature(enable = true) {
|
|
83
143
|
this.enableStreaming = enable;
|
|
84
144
|
if (enable && this.fileManager) {
|
|
85
145
|
this.initializeStreamingComponents();
|
|
86
146
|
}
|
|
87
147
|
else if (!enable) {
|
|
88
|
-
//
|
|
148
|
+
// Cleanup when streaming is disabled
|
|
89
149
|
if (this.realtimeStreamSubscriber) {
|
|
90
150
|
this.streamPublisher.unsubscribe(this.realtimeStreamSubscriber.id);
|
|
91
151
|
this.realtimeStreamSubscriber.destroy();
|
|
@@ -97,12 +157,12 @@ export class ProcessManager {
|
|
|
97
157
|
}
|
|
98
158
|
}
|
|
99
159
|
}
|
|
100
|
-
// Issue #13: RealtimeStreamSubscriber
|
|
160
|
+
// Issue #13: Get RealtimeStreamSubscriber reference (for new MCP tools)
|
|
101
161
|
getRealtimeStreamSubscriber() {
|
|
102
162
|
return this.realtimeStreamSubscriber;
|
|
103
163
|
}
|
|
104
164
|
/**
|
|
105
|
-
|
|
165
|
+
* Issue #13: Get execution ID from output_id
|
|
106
166
|
*/
|
|
107
167
|
findExecutionIdByOutputId(outputId) {
|
|
108
168
|
return this.fileManager?.getExecutionIdByOutputId(outputId);
|
|
@@ -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
|
-
//
|
|
120
|
-
let resolvedInputData =
|
|
212
|
+
// Prepare input data when input_output_id is specified
|
|
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
|
-
// 実行中プロセスの場合: 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
|
-
// 実行中プロセスでない場合、または失敗した場合: 従来のファイル読み取り
|
|
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
|
-
// 実行情報を更新
|
|
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
|
-
// StreamingPipelineReaderがある場合は特別処理
|
|
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
|
-
// エラー時の実行情報更新
|
|
232
314
|
const updatedInfo = this.executions.get(executionId);
|
|
233
315
|
if (updatedInfo) {
|
|
234
316
|
updatedInfo.status = 'failed';
|
|
@@ -239,7 +321,7 @@ export class ProcessManager {
|
|
|
239
321
|
}
|
|
240
322
|
}
|
|
241
323
|
/**
|
|
242
|
-
|
|
324
|
+
* Issue #13: Execute command using StreamingPipelineReader
|
|
243
325
|
*/
|
|
244
326
|
async executeCommandWithInputStream(executionId, options, inputStream) {
|
|
245
327
|
console.error(`ProcessManager: Executing command with input stream for ${executionId}`);
|
|
@@ -248,15 +330,15 @@ export class ProcessManager {
|
|
|
248
330
|
let stdout = '';
|
|
249
331
|
let stderr = '';
|
|
250
332
|
let outputTruncated = false;
|
|
251
|
-
//
|
|
333
|
+
// Prepare environment variables
|
|
252
334
|
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
253
|
-
//
|
|
335
|
+
// Start process
|
|
254
336
|
const child = spawn('sh', ['-c', options.command], {
|
|
255
337
|
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
256
338
|
env,
|
|
257
339
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
258
340
|
});
|
|
259
|
-
// StreamingPipelineReader
|
|
341
|
+
// Connect StreamingPipelineReader to STDIN
|
|
260
342
|
if (child.stdin) {
|
|
261
343
|
inputStream.pipe(child.stdin);
|
|
262
344
|
}
|
|
@@ -264,11 +346,11 @@ export class ProcessManager {
|
|
|
264
346
|
console.error(`StreamingPipelineReader error for ${executionId}: ${error.message}`);
|
|
265
347
|
child.kill('SIGTERM');
|
|
266
348
|
});
|
|
267
|
-
// StreamPublisher
|
|
349
|
+
// Notify StreamPublisher
|
|
268
350
|
if (this.streamPublisher) {
|
|
269
351
|
this.streamPublisher.notifyProcessStart(executionId, options.command);
|
|
270
352
|
}
|
|
271
|
-
// STDOUT
|
|
353
|
+
// Handle STDOUT
|
|
272
354
|
if (child.stdout) {
|
|
273
355
|
child.stdout.on('data', (data) => {
|
|
274
356
|
const chunk = data.toString();
|
|
@@ -278,13 +360,13 @@ export class ProcessManager {
|
|
|
278
360
|
else {
|
|
279
361
|
outputTruncated = true;
|
|
280
362
|
}
|
|
281
|
-
// StreamPublisher
|
|
363
|
+
// Notify StreamPublisher
|
|
282
364
|
if (this.streamPublisher) {
|
|
283
365
|
this.streamPublisher.notifyOutputData(executionId, chunk, false);
|
|
284
366
|
}
|
|
285
367
|
});
|
|
286
368
|
}
|
|
287
|
-
// STDERR
|
|
369
|
+
// Handle STDERR
|
|
288
370
|
if (options.captureStderr && child.stderr) {
|
|
289
371
|
child.stderr.on('data', (data) => {
|
|
290
372
|
const chunk = data.toString();
|
|
@@ -294,29 +376,29 @@ export class ProcessManager {
|
|
|
294
376
|
else {
|
|
295
377
|
outputTruncated = true;
|
|
296
378
|
}
|
|
297
|
-
// StreamPublisher
|
|
379
|
+
// Notify StreamPublisher
|
|
298
380
|
if (this.streamPublisher) {
|
|
299
381
|
this.streamPublisher.notifyOutputData(executionId, chunk, true);
|
|
300
382
|
}
|
|
301
383
|
});
|
|
302
384
|
}
|
|
303
|
-
//
|
|
385
|
+
// Handle process completion
|
|
304
386
|
child.on('close', async (code) => {
|
|
305
387
|
const executionInfo = this.executions.get(executionId);
|
|
306
388
|
if (!executionInfo) {
|
|
307
389
|
reject(new ExecutionError('Execution info not found', { executionId }));
|
|
308
390
|
return;
|
|
309
391
|
}
|
|
310
|
-
//
|
|
392
|
+
// Calculate execution time
|
|
311
393
|
const executionTime = Date.now() - startTime;
|
|
312
|
-
//
|
|
394
|
+
// Update execution info
|
|
313
395
|
executionInfo.status = code === 0 ? 'completed' : 'failed';
|
|
314
396
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
315
397
|
if (code !== null) {
|
|
316
398
|
executionInfo.exit_code = code;
|
|
317
399
|
}
|
|
318
400
|
executionInfo.execution_time_ms = executionTime;
|
|
319
|
-
//
|
|
401
|
+
// Save output
|
|
320
402
|
if (this.fileManager) {
|
|
321
403
|
try {
|
|
322
404
|
const combinedOutput = stdout + (options.captureStderr ? stderr : '');
|
|
@@ -331,7 +413,7 @@ export class ProcessManager {
|
|
|
331
413
|
}
|
|
332
414
|
}
|
|
333
415
|
this.executions.set(executionId, executionInfo);
|
|
334
|
-
// StreamPublisher
|
|
416
|
+
// Notify StreamPublisher
|
|
335
417
|
if (this.streamPublisher) {
|
|
336
418
|
this.streamPublisher.notifyProcessEnd(executionId, code);
|
|
337
419
|
}
|
|
@@ -340,24 +422,28 @@ export class ProcessManager {
|
|
|
340
422
|
});
|
|
341
423
|
child.on('error', (error) => {
|
|
342
424
|
console.error(`Process error for ${executionId}: ${error.message}`);
|
|
343
|
-
// StreamPublisher
|
|
425
|
+
// Notify StreamPublisher
|
|
344
426
|
if (this.streamPublisher) {
|
|
345
427
|
this.streamPublisher.notifyError(executionId, error);
|
|
346
428
|
}
|
|
347
429
|
reject(new ExecutionError(`Process error: ${error.message}`, { originalError: String(error) }));
|
|
348
430
|
});
|
|
349
|
-
//
|
|
350
|
-
const timeout =
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
child.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
431
|
+
// Timeout handling
|
|
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
|
}
|
|
@@ -367,9 +453,9 @@ export class ProcessManager {
|
|
|
367
453
|
let stdout = '';
|
|
368
454
|
let stderr = '';
|
|
369
455
|
let outputTruncated = false;
|
|
370
|
-
//
|
|
456
|
+
// Prepare environment variables
|
|
371
457
|
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
372
|
-
//
|
|
458
|
+
// Start process
|
|
373
459
|
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
374
460
|
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
375
461
|
env,
|
|
@@ -378,48 +464,50 @@ export class ProcessManager {
|
|
|
378
464
|
if (childProcess.pid) {
|
|
379
465
|
this.processes.set(childProcess.pid, childProcess);
|
|
380
466
|
}
|
|
381
|
-
//
|
|
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
|
-
|
|
467
|
+
// Set timeout
|
|
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
|
-
|
|
422
|
-
//
|
|
507
|
+
reject(new TimeoutError(options.timeoutSeconds ?? 0));
|
|
508
|
+
}, options.timeoutSeconds * 1000)
|
|
509
|
+
: undefined;
|
|
510
|
+
// Send stdin
|
|
423
511
|
if (options.inputData) {
|
|
424
512
|
childProcess.stdin?.write(options.inputData);
|
|
425
513
|
childProcess.stdin?.end();
|
|
@@ -427,7 +515,7 @@ export class ProcessManager {
|
|
|
427
515
|
else {
|
|
428
516
|
childProcess.stdin?.end();
|
|
429
517
|
}
|
|
430
|
-
//
|
|
518
|
+
// Handle stdout
|
|
431
519
|
childProcess.stdout?.on('data', (data) => {
|
|
432
520
|
const output = data.toString();
|
|
433
521
|
if (stdout.length + output.length <= options.maxOutputSize) {
|
|
@@ -438,7 +526,7 @@ export class ProcessManager {
|
|
|
438
526
|
outputTruncated = true;
|
|
439
527
|
}
|
|
440
528
|
});
|
|
441
|
-
//
|
|
529
|
+
// Handle stderr
|
|
442
530
|
if (options.captureStderr) {
|
|
443
531
|
childProcess.stderr?.on('data', (data) => {
|
|
444
532
|
const output = data.toString();
|
|
@@ -451,9 +539,11 @@ export class ProcessManager {
|
|
|
451
539
|
}
|
|
452
540
|
});
|
|
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
|
}
|
|
@@ -469,30 +559,30 @@ export class ProcessManager {
|
|
|
469
559
|
executionInfo.process_id = childProcess.pid;
|
|
470
560
|
}
|
|
471
561
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
472
|
-
//
|
|
562
|
+
// Save output to FileManager (regardless of size)
|
|
473
563
|
let outputFileId;
|
|
474
564
|
try {
|
|
475
565
|
outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
476
566
|
executionInfo.output_id = outputFileId;
|
|
477
567
|
}
|
|
478
568
|
catch (error) {
|
|
479
|
-
//
|
|
569
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
480
570
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
481
571
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
482
572
|
}
|
|
483
|
-
//
|
|
573
|
+
// Set detailed output status
|
|
484
574
|
if (outputTruncated) {
|
|
485
575
|
this.setOutputStatus(executionInfo, true, 'size_limit', outputFileId);
|
|
486
576
|
}
|
|
487
577
|
else {
|
|
488
|
-
//
|
|
578
|
+
// Normal completion: actuallyTruncated=false, use a valid reason to show completion guidance
|
|
489
579
|
this.setOutputStatus(executionInfo, false, 'size_limit', outputFileId);
|
|
490
580
|
}
|
|
491
581
|
this.executions.set(executionId, executionInfo);
|
|
492
582
|
resolve(executionInfo);
|
|
493
583
|
}
|
|
494
584
|
});
|
|
495
|
-
//
|
|
585
|
+
// Error handling
|
|
496
586
|
childProcess.on('error', (error) => {
|
|
497
587
|
clearTimeout(timeout);
|
|
498
588
|
if (childProcess.pid) {
|
|
@@ -512,9 +602,9 @@ export class ProcessManager {
|
|
|
512
602
|
});
|
|
513
603
|
}
|
|
514
604
|
async executeAdaptiveCommand(executionId, options) {
|
|
515
|
-
//
|
|
516
|
-
// 1.
|
|
517
|
-
// 2.
|
|
605
|
+
// Adaptive mode: start one process and transition to background when:
|
|
606
|
+
// 1. Foreground timeout is reached
|
|
607
|
+
// 2. Output size limit is reached
|
|
518
608
|
const returnPartialOnTimeout = options.returnPartialOnTimeout ?? true;
|
|
519
609
|
const foregroundTimeout = options.foregroundTimeoutSeconds ?? 10;
|
|
520
610
|
return new Promise((resolve, reject) => {
|
|
@@ -523,9 +613,9 @@ export class ProcessManager {
|
|
|
523
613
|
let stderr = '';
|
|
524
614
|
let outputTruncated = false;
|
|
525
615
|
let backgroundTransitionReason = null;
|
|
526
|
-
//
|
|
616
|
+
// Prepare environment variables
|
|
527
617
|
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
528
|
-
//
|
|
618
|
+
// Start process (supports background transition)
|
|
529
619
|
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
530
620
|
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
531
621
|
env,
|
|
@@ -534,48 +624,50 @@ export class ProcessManager {
|
|
|
534
624
|
if (childProcess.pid) {
|
|
535
625
|
this.processes.set(childProcess.pid, childProcess);
|
|
536
626
|
}
|
|
537
|
-
//
|
|
627
|
+
// Set foreground timeout
|
|
538
628
|
const foregroundTimeoutHandle = setTimeout(() => {
|
|
539
629
|
if (!backgroundTransitionReason) {
|
|
540
630
|
backgroundTransitionReason = 'timeout';
|
|
541
631
|
transitionToBackground();
|
|
542
632
|
}
|
|
543
633
|
}, foregroundTimeout * 1000);
|
|
544
|
-
//
|
|
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
|
-
|
|
634
|
+
// Set final timeout
|
|
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
|
-
|
|
578
|
-
//
|
|
667
|
+
reject(new TimeoutError(options.timeoutSeconds ?? 0));
|
|
668
|
+
}, options.timeoutSeconds * 1000)
|
|
669
|
+
: undefined;
|
|
670
|
+
// Function to transition to background mode
|
|
579
671
|
const transitionToBackground = async () => {
|
|
580
672
|
clearTimeout(foregroundTimeoutHandle);
|
|
581
673
|
const executionInfo = this.executions.get(executionId);
|
|
@@ -583,7 +675,7 @@ export class ProcessManager {
|
|
|
583
675
|
executionInfo.status = 'running';
|
|
584
676
|
executionInfo.stdout = sanitizeString(stdout);
|
|
585
677
|
executionInfo.stderr = sanitizeString(stderr);
|
|
586
|
-
//
|
|
678
|
+
// Record transition reason
|
|
587
679
|
if (backgroundTransitionReason === 'timeout') {
|
|
588
680
|
executionInfo.transition_reason = 'foreground_timeout';
|
|
589
681
|
}
|
|
@@ -593,29 +685,34 @@ export class ProcessManager {
|
|
|
593
685
|
if (childProcess.pid !== undefined) {
|
|
594
686
|
executionInfo.process_id = childProcess.pid;
|
|
595
687
|
}
|
|
596
|
-
//
|
|
688
|
+
// Save output to FileManager
|
|
597
689
|
let outputFileId;
|
|
598
690
|
try {
|
|
599
691
|
outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
600
692
|
executionInfo.output_id = outputFileId;
|
|
601
693
|
}
|
|
602
694
|
catch (error) {
|
|
603
|
-
//
|
|
695
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
604
696
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
605
697
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
606
698
|
}
|
|
607
|
-
//
|
|
699
|
+
// Set detailed output status (background transition)
|
|
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
|
}
|
|
617
714
|
};
|
|
618
|
-
//
|
|
715
|
+
// Send stdin
|
|
619
716
|
if (options.inputData) {
|
|
620
717
|
childProcess.stdin?.write(options.inputData);
|
|
621
718
|
childProcess.stdin?.end();
|
|
@@ -623,7 +720,7 @@ export class ProcessManager {
|
|
|
623
720
|
else {
|
|
624
721
|
childProcess.stdin?.end();
|
|
625
722
|
}
|
|
626
|
-
//
|
|
723
|
+
// Handle stdout
|
|
627
724
|
childProcess.stdout?.on('data', (data) => {
|
|
628
725
|
const output = data.toString();
|
|
629
726
|
if (stdout.length + output.length <= options.maxOutputSize) {
|
|
@@ -632,14 +729,14 @@ export class ProcessManager {
|
|
|
632
729
|
else {
|
|
633
730
|
stdout += output.substring(0, options.maxOutputSize - stdout.length);
|
|
634
731
|
outputTruncated = true;
|
|
635
|
-
//
|
|
732
|
+
// Transition to background when output size limit is reached
|
|
636
733
|
if (!backgroundTransitionReason) {
|
|
637
734
|
backgroundTransitionReason = 'output_size_limit';
|
|
638
735
|
transitionToBackground();
|
|
639
736
|
}
|
|
640
737
|
}
|
|
641
738
|
});
|
|
642
|
-
//
|
|
739
|
+
// Handle stderr
|
|
643
740
|
if (options.captureStderr) {
|
|
644
741
|
childProcess.stderr?.on('data', (data) => {
|
|
645
742
|
const output = data.toString();
|
|
@@ -649,7 +746,7 @@ export class ProcessManager {
|
|
|
649
746
|
else {
|
|
650
747
|
stderr += output.substring(0, options.maxOutputSize - stderr.length);
|
|
651
748
|
outputTruncated = true;
|
|
652
|
-
//
|
|
749
|
+
// Transition to background when output size limit is reached
|
|
653
750
|
if (!backgroundTransitionReason) {
|
|
654
751
|
backgroundTransitionReason = 'output_size_limit';
|
|
655
752
|
transitionToBackground();
|
|
@@ -657,14 +754,16 @@ export class ProcessManager {
|
|
|
657
754
|
}
|
|
658
755
|
});
|
|
659
756
|
}
|
|
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
|
}
|
|
667
|
-
//
|
|
766
|
+
// Handle only when no background transition occurred
|
|
668
767
|
if (!backgroundTransitionReason) {
|
|
669
768
|
const executionTime = Date.now() - startTime;
|
|
670
769
|
const executionInfo = this.executions.get(executionId);
|
|
@@ -679,13 +778,13 @@ export class ProcessManager {
|
|
|
679
778
|
executionInfo.process_id = childProcess.pid;
|
|
680
779
|
}
|
|
681
780
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
682
|
-
//
|
|
781
|
+
// Save output to FileManager
|
|
683
782
|
try {
|
|
684
783
|
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
685
784
|
executionInfo.output_id = outputFileId;
|
|
686
785
|
}
|
|
687
786
|
catch (error) {
|
|
688
|
-
//
|
|
787
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
689
788
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
690
789
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
691
790
|
}
|
|
@@ -694,10 +793,12 @@ export class ProcessManager {
|
|
|
694
793
|
}
|
|
695
794
|
}
|
|
696
795
|
});
|
|
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
|
}
|
|
@@ -732,7 +833,7 @@ export class ProcessManager {
|
|
|
732
833
|
executionInfo.process_id = childProcess.pid;
|
|
733
834
|
this.executions.set(executionId, executionInfo);
|
|
734
835
|
}
|
|
735
|
-
//
|
|
836
|
+
// For background processes, handle output asynchronously
|
|
736
837
|
if (options.executionMode === 'background') {
|
|
737
838
|
this.handleBackgroundProcess(executionId, childProcess, options);
|
|
738
839
|
}
|
|
@@ -746,54 +847,56 @@ export class ProcessManager {
|
|
|
746
847
|
const startTime = Date.now();
|
|
747
848
|
let stdout = '';
|
|
748
849
|
let stderr = '';
|
|
749
|
-
//
|
|
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
|
-
|
|
850
|
+
// Set timeout (for background processes)
|
|
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
|
-
|
|
796
|
-
//
|
|
897
|
+
}, options.timeoutSeconds * 1000)
|
|
898
|
+
: undefined;
|
|
899
|
+
// Collect output
|
|
797
900
|
childProcess.stdout?.on('data', (data) => {
|
|
798
901
|
stdout += data.toString();
|
|
799
902
|
});
|
|
@@ -802,9 +905,11 @@ export class ProcessManager {
|
|
|
802
905
|
stderr += data.toString();
|
|
803
906
|
});
|
|
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
|
}
|
|
@@ -814,18 +919,18 @@ export class ProcessManager {
|
|
|
814
919
|
executionInfo.exit_code = code || 0;
|
|
815
920
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
816
921
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
817
|
-
//
|
|
922
|
+
// Save output to file
|
|
818
923
|
try {
|
|
819
924
|
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
820
925
|
executionInfo.output_id = outputFileId;
|
|
821
926
|
}
|
|
822
927
|
catch (error) {
|
|
823
|
-
//
|
|
928
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
824
929
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
825
930
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
826
931
|
}
|
|
827
932
|
this.executions.set(executionId, executionInfo);
|
|
828
|
-
//
|
|
933
|
+
// Invoke completion callback for background process
|
|
829
934
|
if (this.backgroundProcessCallbacks.onComplete) {
|
|
830
935
|
setImmediate(async () => {
|
|
831
936
|
try {
|
|
@@ -838,7 +943,7 @@ export class ProcessManager {
|
|
|
838
943
|
}
|
|
839
944
|
}
|
|
840
945
|
catch (callbackError) {
|
|
841
|
-
//
|
|
946
|
+
// Record callback errors in internal logs only
|
|
842
947
|
// console.error('Background process completion callback error:', callbackError);
|
|
843
948
|
}
|
|
844
949
|
});
|
|
@@ -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
|
}
|
|
@@ -856,7 +963,7 @@ export class ProcessManager {
|
|
|
856
963
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
857
964
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
858
965
|
this.executions.set(executionId, executionInfo);
|
|
859
|
-
//
|
|
966
|
+
// Invoke error callback for background process
|
|
860
967
|
if (this.backgroundProcessCallbacks.onError) {
|
|
861
968
|
setImmediate(async () => {
|
|
862
969
|
try {
|
|
@@ -869,7 +976,7 @@ export class ProcessManager {
|
|
|
869
976
|
}
|
|
870
977
|
}
|
|
871
978
|
catch (callbackError) {
|
|
872
|
-
//
|
|
979
|
+
// Record callback errors in internal logs only
|
|
873
980
|
// console.error('Background process error callback error:', callbackError);
|
|
874
981
|
}
|
|
875
982
|
});
|
|
@@ -877,27 +984,31 @@ export class ProcessManager {
|
|
|
877
984
|
}
|
|
878
985
|
});
|
|
879
986
|
}
|
|
880
|
-
// adaptive mode
|
|
987
|
+
// Handle processes transitioned to background in adaptive mode
|
|
881
988
|
handleAdaptiveBackgroundTransition(executionId, childProcess, options) {
|
|
882
|
-
//
|
|
883
|
-
const timeout =
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
childProcess.
|
|
989
|
+
// Set timeout (final timeout)
|
|
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
|
-
|
|
892
|
-
executionInfo.status = 'timeout';
|
|
893
|
-
executionInfo.completed_at = getCurrentTimestamp();
|
|
894
|
-
// 既存の出力は保持(adaptive modeで既にキャプチャ済み)
|
|
895
|
-
this.executions.set(executionId, executionInfo);
|
|
896
|
-
}
|
|
897
|
-
}, options.timeoutSeconds * 1000);
|
|
898
|
-
// プロセス終了時の処理
|
|
1005
|
+
}, options.timeoutSeconds * 1000)
|
|
1006
|
+
: undefined;
|
|
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
|
}
|
|
@@ -906,13 +1017,13 @@ export class ProcessManager {
|
|
|
906
1017
|
executionInfo.status = 'completed';
|
|
907
1018
|
executionInfo.exit_code = code || 0;
|
|
908
1019
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
909
|
-
//
|
|
1020
|
+
// Calculate total execution time (foreground + background)
|
|
910
1021
|
if (executionInfo.started_at) {
|
|
911
1022
|
const startTime = new Date(executionInfo.started_at).getTime();
|
|
912
1023
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
913
1024
|
}
|
|
914
1025
|
this.executions.set(executionId, executionInfo);
|
|
915
|
-
// adaptive
|
|
1026
|
+
// Invoke completion callback for adaptive-mode background process
|
|
916
1027
|
if (this.backgroundProcessCallbacks.onComplete) {
|
|
917
1028
|
setImmediate(async () => {
|
|
918
1029
|
try {
|
|
@@ -925,7 +1036,7 @@ export class ProcessManager {
|
|
|
925
1036
|
}
|
|
926
1037
|
}
|
|
927
1038
|
catch (callbackError) {
|
|
928
|
-
//
|
|
1039
|
+
// Record callback errors in internal logs only
|
|
929
1040
|
// console.error('Adaptive background process completion callback error:', callbackError);
|
|
930
1041
|
}
|
|
931
1042
|
});
|
|
@@ -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
|
}
|
|
@@ -946,7 +1059,7 @@ export class ProcessManager {
|
|
|
946
1059
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
947
1060
|
}
|
|
948
1061
|
this.executions.set(executionId, executionInfo);
|
|
949
|
-
// adaptive
|
|
1062
|
+
// Invoke error callback for adaptive-mode background process
|
|
950
1063
|
if (this.backgroundProcessCallbacks.onError) {
|
|
951
1064
|
setImmediate(async () => {
|
|
952
1065
|
try {
|
|
@@ -959,7 +1072,7 @@ export class ProcessManager {
|
|
|
959
1072
|
}
|
|
960
1073
|
}
|
|
961
1074
|
catch (callbackError) {
|
|
962
|
-
//
|
|
1075
|
+
// Record callback errors in internal logs only
|
|
963
1076
|
// console.error('Adaptive background process error callback error:', callbackError);
|
|
964
1077
|
}
|
|
965
1078
|
});
|
|
@@ -968,23 +1081,23 @@ export class ProcessManager {
|
|
|
968
1081
|
});
|
|
969
1082
|
}
|
|
970
1083
|
async executeDetachedCommand(executionId, options) {
|
|
971
|
-
//
|
|
1084
|
+
// Detached mode: run fully in background and detach from parent process
|
|
972
1085
|
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
973
1086
|
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
974
1087
|
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
975
1088
|
env,
|
|
976
|
-
stdio: ['ignore', 'pipe', 'pipe'], // stdin
|
|
977
|
-
detached: true, //
|
|
1089
|
+
stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin
|
|
1090
|
+
detached: true, // fully detached
|
|
978
1091
|
});
|
|
979
|
-
//
|
|
1092
|
+
// Record detached process PID but exclude it from process management
|
|
980
1093
|
const executionInfo = this.executions.get(executionId);
|
|
981
1094
|
if (executionInfo && childProcess.pid !== undefined) {
|
|
982
1095
|
executionInfo.process_id = childProcess.pid;
|
|
983
1096
|
executionInfo.status = 'running';
|
|
984
1097
|
this.executions.set(executionId, executionInfo);
|
|
985
1098
|
}
|
|
986
|
-
//
|
|
987
|
-
//
|
|
1099
|
+
// Detached processes continue after parent exits,
|
|
1100
|
+
// so output collection is limited
|
|
988
1101
|
const startTime = Date.now();
|
|
989
1102
|
let stdout = '';
|
|
990
1103
|
let stderr = '';
|
|
@@ -998,7 +1111,7 @@ export class ProcessManager {
|
|
|
998
1111
|
stderr += data.toString();
|
|
999
1112
|
});
|
|
1000
1113
|
}
|
|
1001
|
-
//
|
|
1114
|
+
// Monitor process exit (not always capturable when detached)
|
|
1002
1115
|
childProcess.on('close', async (code) => {
|
|
1003
1116
|
const executionInfo = this.executions.get(executionId);
|
|
1004
1117
|
if (executionInfo) {
|
|
@@ -1011,12 +1124,12 @@ export class ProcessManager {
|
|
|
1011
1124
|
executionInfo.output_id = outputFileId;
|
|
1012
1125
|
}
|
|
1013
1126
|
catch (error) {
|
|
1014
|
-
//
|
|
1127
|
+
// Record file-save failures as critical errors and include them in execution info
|
|
1015
1128
|
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
1016
1129
|
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1017
1130
|
}
|
|
1018
1131
|
this.executions.set(executionId, executionInfo);
|
|
1019
|
-
// detached
|
|
1132
|
+
// Invoke completion callback for detached process
|
|
1020
1133
|
if (this.backgroundProcessCallbacks.onComplete) {
|
|
1021
1134
|
setImmediate(async () => {
|
|
1022
1135
|
try {
|
|
@@ -1029,7 +1142,7 @@ export class ProcessManager {
|
|
|
1029
1142
|
}
|
|
1030
1143
|
}
|
|
1031
1144
|
catch (callbackError) {
|
|
1032
|
-
//
|
|
1145
|
+
// Record callback errors in internal logs only
|
|
1033
1146
|
// console.error('Detached process completion callback error:', callbackError);
|
|
1034
1147
|
}
|
|
1035
1148
|
});
|
|
@@ -1043,7 +1156,7 @@ export class ProcessManager {
|
|
|
1043
1156
|
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
1044
1157
|
executionInfo.completed_at = getCurrentTimestamp();
|
|
1045
1158
|
this.executions.set(executionId, executionInfo);
|
|
1046
|
-
// detached
|
|
1159
|
+
// Invoke error callback for detached process
|
|
1047
1160
|
if (this.backgroundProcessCallbacks.onError) {
|
|
1048
1161
|
setImmediate(async () => {
|
|
1049
1162
|
try {
|
|
@@ -1056,14 +1169,14 @@ export class ProcessManager {
|
|
|
1056
1169
|
}
|
|
1057
1170
|
}
|
|
1058
1171
|
catch (callbackError) {
|
|
1059
|
-
//
|
|
1172
|
+
// Record callback errors in internal logs only
|
|
1060
1173
|
// console.error('Detached process error callback error:', callbackError);
|
|
1061
1174
|
}
|
|
1062
1175
|
});
|
|
1063
1176
|
}
|
|
1064
1177
|
}
|
|
1065
1178
|
});
|
|
1066
|
-
//
|
|
1179
|
+
// Detach process
|
|
1067
1180
|
childProcess.unref();
|
|
1068
1181
|
const resultExecutionInfo = this.executions.get(executionId);
|
|
1069
1182
|
if (!resultExecutionInfo) {
|
|
@@ -1073,7 +1186,7 @@ export class ProcessManager {
|
|
|
1073
1186
|
}
|
|
1074
1187
|
async saveOutputToFile(executionId, stdout, stderr) {
|
|
1075
1188
|
if (!this.fileManager) {
|
|
1076
|
-
// FileManager
|
|
1189
|
+
// If FileManager is unavailable, save file using legacy method
|
|
1077
1190
|
const outputFileId = generateId();
|
|
1078
1191
|
const filePath = path.join(this.outputDir, `${outputFileId}.json`);
|
|
1079
1192
|
const outputData = {
|
|
@@ -1085,27 +1198,27 @@ export class ProcessManager {
|
|
|
1085
1198
|
await fs.writeFile(filePath, JSON.stringify(outputData, null, 2), 'utf-8');
|
|
1086
1199
|
return outputFileId;
|
|
1087
1200
|
}
|
|
1088
|
-
// FileManager
|
|
1201
|
+
// Create output file using FileManager
|
|
1089
1202
|
const combinedOutput = stdout + (stderr ? '\n--- STDERR ---\n' + stderr : '');
|
|
1090
1203
|
return await this.fileManager.createOutputFile(combinedOutput, executionId);
|
|
1091
1204
|
}
|
|
1092
1205
|
/**
|
|
1093
|
-
|
|
1206
|
+
* Helper to set detailed output status information
|
|
1094
1207
|
* Issue #14: Enhanced guidance messages for adaptive mode transitions
|
|
1095
|
-
|
|
1208
|
+
* Improvement: determine status by reason instead of outputTruncated
|
|
1096
1209
|
*/
|
|
1097
|
-
setOutputStatus(executionInfo, actuallyTruncated, //
|
|
1210
|
+
setOutputStatus(executionInfo, actuallyTruncated, // whether output was actually truncated
|
|
1098
1211
|
reason, outputId) {
|
|
1099
|
-
// reason
|
|
1100
|
-
const needsGuidance = !!outputId; // output_id
|
|
1101
|
-
//
|
|
1212
|
+
// Set output status based on reason
|
|
1213
|
+
const needsGuidance = !!outputId; // always provide guidance when output_id exists
|
|
1214
|
+
// Set outputTruncated for backward compatibility
|
|
1102
1215
|
executionInfo.output_truncated =
|
|
1103
1216
|
actuallyTruncated || reason === 'timeout' || reason === 'background_transition';
|
|
1104
|
-
// Issue #14:
|
|
1217
|
+
// Issue #14: Handle background transitions and timeouts specially
|
|
1105
1218
|
if (reason === 'background_transition') {
|
|
1106
1219
|
executionInfo.truncation_reason = reason;
|
|
1107
1220
|
executionInfo.output_status = {
|
|
1108
|
-
complete: false, //
|
|
1221
|
+
complete: false, // incomplete while running in background
|
|
1109
1222
|
reason: reason,
|
|
1110
1223
|
available_via_output_id: !!outputId,
|
|
1111
1224
|
recommended_action: outputId ? 'use_read_execution_output' : undefined,
|
|
@@ -1136,7 +1249,7 @@ export class ProcessManager {
|
|
|
1136
1249
|
if (reason === 'timeout') {
|
|
1137
1250
|
executionInfo.truncation_reason = reason;
|
|
1138
1251
|
executionInfo.output_status = {
|
|
1139
|
-
complete: false, //
|
|
1252
|
+
complete: false, // timeout means incomplete
|
|
1140
1253
|
reason: reason,
|
|
1141
1254
|
available_via_output_id: !!outputId,
|
|
1142
1255
|
recommended_action: outputId ? 'use_read_execution_output' : undefined,
|
|
@@ -1158,7 +1271,7 @@ export class ProcessManager {
|
|
|
1158
1271
|
}
|
|
1159
1272
|
return;
|
|
1160
1273
|
}
|
|
1161
|
-
//
|
|
1274
|
+
// When output was actually truncated
|
|
1162
1275
|
if (actuallyTruncated) {
|
|
1163
1276
|
executionInfo.truncation_reason = reason;
|
|
1164
1277
|
executionInfo.output_status = {
|
|
@@ -1167,7 +1280,7 @@ export class ProcessManager {
|
|
|
1167
1280
|
available_via_output_id: !!outputId,
|
|
1168
1281
|
recommended_action: outputId ? 'use_read_execution_output' : undefined,
|
|
1169
1282
|
};
|
|
1170
|
-
//
|
|
1283
|
+
// Set message and actions based on situation
|
|
1171
1284
|
switch (reason) {
|
|
1172
1285
|
case 'size_limit':
|
|
1173
1286
|
executionInfo.message = `Output exceeded size limit. ${outputId ? 'Complete output available via output_id.' : 'Output was truncated.'}`;
|
|
@@ -1205,7 +1318,7 @@ export class ProcessManager {
|
|
|
1205
1318
|
}
|
|
1206
1319
|
}
|
|
1207
1320
|
else {
|
|
1208
|
-
//
|
|
1321
|
+
// Completed case (no truncation)
|
|
1209
1322
|
executionInfo.output_status = {
|
|
1210
1323
|
complete: true,
|
|
1211
1324
|
available_via_output_id: !!outputId,
|
|
@@ -1228,7 +1341,7 @@ export class ProcessManager {
|
|
|
1228
1341
|
}
|
|
1229
1342
|
listExecutions(filter) {
|
|
1230
1343
|
let executions = Array.from(this.executions.values());
|
|
1231
|
-
//
|
|
1344
|
+
// Filtering
|
|
1232
1345
|
if (filter) {
|
|
1233
1346
|
if (filter.status) {
|
|
1234
1347
|
executions = executions.filter((exec) => exec.status === filter.status);
|
|
@@ -1238,11 +1351,11 @@ export class ProcessManager {
|
|
|
1238
1351
|
executions = executions.filter((exec) => pattern.test(exec.command));
|
|
1239
1352
|
}
|
|
1240
1353
|
if (filter.sessionId) {
|
|
1241
|
-
//
|
|
1354
|
+
// Session management will be implemented later
|
|
1242
1355
|
}
|
|
1243
1356
|
}
|
|
1244
1357
|
const total = executions.length;
|
|
1245
|
-
//
|
|
1358
|
+
// Pagination
|
|
1246
1359
|
if (filter?.offset || filter?.limit) {
|
|
1247
1360
|
const offset = filter.offset || 0;
|
|
1248
1361
|
const limit = filter.limit || 50;
|
|
@@ -1256,17 +1369,17 @@ export class ProcessManager {
|
|
|
1256
1369
|
throw new ResourceNotFoundError('process', processId.toString());
|
|
1257
1370
|
}
|
|
1258
1371
|
try {
|
|
1259
|
-
//
|
|
1372
|
+
// Terminate process
|
|
1260
1373
|
const signalName = signal === 'KILL' ? 'SIGKILL' : `SIG${signal}`;
|
|
1261
1374
|
const killed = childProcess.kill(signalName);
|
|
1262
1375
|
if (!killed && force && signal !== 'KILL') {
|
|
1263
|
-
//
|
|
1376
|
+
// Force kill
|
|
1264
1377
|
childProcess.kill('SIGKILL');
|
|
1265
1378
|
}
|
|
1266
|
-
//
|
|
1379
|
+
// Wait until process exits
|
|
1267
1380
|
await new Promise((resolve) => {
|
|
1268
1381
|
childProcess.on('close', () => resolve());
|
|
1269
|
-
setTimeout(() => resolve(), 5000); // 5
|
|
1382
|
+
setTimeout(() => resolve(), 5000); // timeout after 5 seconds
|
|
1270
1383
|
});
|
|
1271
1384
|
this.processes.delete(processId);
|
|
1272
1385
|
return {
|
|
@@ -1287,7 +1400,7 @@ export class ProcessManager {
|
|
|
1287
1400
|
listProcesses() {
|
|
1288
1401
|
const processes = [];
|
|
1289
1402
|
for (const [pid] of this.processes) {
|
|
1290
|
-
//
|
|
1403
|
+
// Find corresponding execution info
|
|
1291
1404
|
const execution = Array.from(this.executions.values()).find((exec) => exec.process_id === pid);
|
|
1292
1405
|
if (execution) {
|
|
1293
1406
|
const processInfo = {
|
|
@@ -1315,7 +1428,7 @@ export class ProcessManager {
|
|
|
1315
1428
|
return processes;
|
|
1316
1429
|
}
|
|
1317
1430
|
cleanup() {
|
|
1318
|
-
//
|
|
1431
|
+
// Terminate all running processes
|
|
1319
1432
|
for (const [, childProcess] of this.processes) {
|
|
1320
1433
|
try {
|
|
1321
1434
|
childProcess.kill('SIGTERM');
|
|
@@ -1326,17 +1439,17 @@ export class ProcessManager {
|
|
|
1326
1439
|
}, 5000);
|
|
1327
1440
|
}
|
|
1328
1441
|
catch (error) {
|
|
1329
|
-
//
|
|
1442
|
+
// Record error in internal log (avoid stdout)
|
|
1330
1443
|
// console.error(`Failed to cleanup process ${pid}:`, error);
|
|
1331
1444
|
}
|
|
1332
1445
|
}
|
|
1333
1446
|
this.processes.clear();
|
|
1334
1447
|
this.executions.clear();
|
|
1335
1448
|
}
|
|
1336
|
-
//
|
|
1449
|
+
// Working directory management
|
|
1337
1450
|
setDefaultWorkingDirectory(workingDirectory) {
|
|
1338
1451
|
const previousWorkdir = this.defaultWorkingDirectory;
|
|
1339
|
-
//
|
|
1452
|
+
// Validate directory
|
|
1340
1453
|
if (!this.isAllowedWorkingDirectory(workingDirectory)) {
|
|
1341
1454
|
throw new Error(`Working directory not allowed: ${workingDirectory}`);
|
|
1342
1455
|
}
|
|
@@ -1355,7 +1468,7 @@ export class ProcessManager {
|
|
|
1355
1468
|
return [...this.allowedWorkingDirectories];
|
|
1356
1469
|
}
|
|
1357
1470
|
isAllowedWorkingDirectory(workingDirectory) {
|
|
1358
|
-
//
|
|
1471
|
+
// Compare using normalized paths
|
|
1359
1472
|
const normalizedPath = path.resolve(workingDirectory);
|
|
1360
1473
|
return this.allowedWorkingDirectories.some((allowedDir) => {
|
|
1361
1474
|
const normalizedAllowed = path.resolve(allowedDir);
|