@mako10k/shell-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/backoffice/index.d.ts +2 -0
- package/dist/backoffice/index.d.ts.map +1 -0
- package/dist/backoffice/index.js +47 -0
- package/dist/backoffice/index.js.map +1 -0
- package/dist/backoffice/server.d.ts +45 -0
- package/dist/backoffice/server.d.ts.map +1 -0
- package/dist/backoffice/server.js +610 -0
- package/dist/backoffice/server.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +525 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/config-manager.d.ts +80 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +218 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/enhanced-history-manager.d.ts +84 -0
- package/dist/core/enhanced-history-manager.d.ts.map +1 -0
- package/dist/core/enhanced-history-manager.js +319 -0
- package/dist/core/enhanced-history-manager.js.map +1 -0
- package/dist/core/file-manager.d.ts +79 -0
- package/dist/core/file-manager.d.ts.map +1 -0
- package/dist/core/file-manager.js +338 -0
- package/dist/core/file-manager.js.map +1 -0
- package/dist/core/file-storage-subscriber.d.ts +38 -0
- package/dist/core/file-storage-subscriber.d.ts.map +1 -0
- package/dist/core/file-storage-subscriber.js +132 -0
- package/dist/core/file-storage-subscriber.js.map +1 -0
- package/dist/core/monitoring-manager.d.ts +32 -0
- package/dist/core/monitoring-manager.d.ts.map +1 -0
- package/dist/core/monitoring-manager.js +296 -0
- package/dist/core/monitoring-manager.js.map +1 -0
- package/dist/core/process-manager.d.ts +105 -0
- package/dist/core/process-manager.d.ts.map +1 -0
- package/dist/core/process-manager.js +1374 -0
- package/dist/core/process-manager.js.map +1 -0
- package/dist/core/realtime-stream-subscriber.d.ts +93 -0
- package/dist/core/realtime-stream-subscriber.d.ts.map +1 -0
- package/dist/core/realtime-stream-subscriber.js +200 -0
- package/dist/core/realtime-stream-subscriber.js.map +1 -0
- package/dist/core/remote-http-client.d.ts +15 -0
- package/dist/core/remote-http-client.d.ts.map +1 -0
- package/dist/core/remote-http-client.js +60 -0
- package/dist/core/remote-http-client.js.map +1 -0
- package/dist/core/remote-process-service.d.ts +50 -0
- package/dist/core/remote-process-service.d.ts.map +1 -0
- package/dist/core/remote-process-service.js +20 -0
- package/dist/core/remote-process-service.js.map +1 -0
- package/dist/core/server-manager.d.ts +71 -0
- package/dist/core/server-manager.d.ts.map +1 -0
- package/dist/core/server-manager.js +680 -0
- package/dist/core/server-manager.js.map +1 -0
- package/dist/core/stream-publisher.d.ts +75 -0
- package/dist/core/stream-publisher.d.ts.map +1 -0
- package/dist/core/stream-publisher.js +127 -0
- package/dist/core/stream-publisher.js.map +1 -0
- package/dist/core/streaming-pipeline-reader.d.ts +67 -0
- package/dist/core/streaming-pipeline-reader.d.ts.map +1 -0
- package/dist/core/streaming-pipeline-reader.js +191 -0
- package/dist/core/streaming-pipeline-reader.js.map +1 -0
- package/dist/core/terminal-manager.d.ts +96 -0
- package/dist/core/terminal-manager.d.ts.map +1 -0
- package/dist/core/terminal-manager.js +515 -0
- package/dist/core/terminal-manager.js.map +1 -0
- package/dist/daemon/server.d.ts +8 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +416 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/uds-transport.d.ts +31 -0
- package/dist/daemon/uds-transport.d.ts.map +1 -0
- package/dist/daemon/uds-transport.js +149 -0
- package/dist/daemon/uds-transport.js.map +1 -0
- package/dist/executor/server.d.ts +20 -0
- package/dist/executor/server.d.ts.map +1 -0
- package/dist/executor/server.js +375 -0
- package/dist/executor/server.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/daemon-runtime.d.ts +4 -0
- package/dist/runtime/daemon-runtime.d.ts.map +1 -0
- package/dist/runtime/daemon-runtime.js +4 -0
- package/dist/runtime/daemon-runtime.js.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/tool-runtime.d.ts +52 -0
- package/dist/runtime/tool-runtime.d.ts.map +1 -0
- package/dist/runtime/tool-runtime.js +161 -0
- package/dist/runtime/tool-runtime.js.map +1 -0
- package/dist/security/chat-completion-adapter.d.ts +443 -0
- package/dist/security/chat-completion-adapter.d.ts.map +1 -0
- package/dist/security/chat-completion-adapter.js +475 -0
- package/dist/security/chat-completion-adapter.js.map +1 -0
- package/dist/security/enhanced-evaluator.d.ts +139 -0
- package/dist/security/enhanced-evaluator.d.ts.map +1 -0
- package/dist/security/enhanced-evaluator.js +1208 -0
- package/dist/security/enhanced-evaluator.js.map +1 -0
- package/dist/security/evaluator-types.d.ts +614 -0
- package/dist/security/evaluator-types.d.ts.map +1 -0
- package/dist/security/evaluator-types.js +124 -0
- package/dist/security/evaluator-types.js.map +1 -0
- package/dist/security/manager.d.ts +76 -0
- package/dist/security/manager.d.ts.map +1 -0
- package/dist/security/manager.js +445 -0
- package/dist/security/manager.js.map +1 -0
- package/dist/security/security-llm-prompt-generator.d.ts +105 -0
- package/dist/security/security-llm-prompt-generator.d.ts.map +1 -0
- package/dist/security/security-llm-prompt-generator.js +323 -0
- package/dist/security/security-llm-prompt-generator.js.map +1 -0
- package/dist/security/security-tools.d.ts +174 -0
- package/dist/security/security-tools.d.ts.map +1 -0
- package/dist/security/security-tools.js +159 -0
- package/dist/security/security-tools.js.map +1 -0
- package/dist/security/validator-criteria-manager.d.ts +47 -0
- package/dist/security/validator-criteria-manager.d.ts.map +1 -0
- package/dist/security/validator-criteria-manager.js +169 -0
- package/dist/security/validator-criteria-manager.js.map +1 -0
- package/dist/tools/shell-tools.d.ts +474 -0
- package/dist/tools/shell-tools.d.ts.map +1 -0
- package/dist/tools/shell-tools.js +861 -0
- package/dist/tools/shell-tools.js.map +1 -0
- package/dist/types/enhanced-security.d.ts +529 -0
- package/dist/types/enhanced-security.d.ts.map +1 -0
- package/dist/types/enhanced-security.js +286 -0
- package/dist/types/enhanced-security.js.map +1 -0
- package/dist/types/index.d.ts +282 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +158 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/quick-schemas.d.ts +177 -0
- package/dist/types/quick-schemas.d.ts.map +1 -0
- package/dist/types/quick-schemas.js +113 -0
- package/dist/types/quick-schemas.js.map +1 -0
- package/dist/types/response-schemas.d.ts +41 -0
- package/dist/types/response-schemas.d.ts.map +1 -0
- package/dist/types/response-schemas.js +41 -0
- package/dist/types/response-schemas.js.map +1 -0
- package/dist/types/schemas.d.ts +578 -0
- package/dist/types/schemas.d.ts.map +1 -0
- package/dist/types/schemas.js +498 -0
- package/dist/types/schemas.js.map +1 -0
- package/dist/utils/criteria-manager.d.ts +47 -0
- package/dist/utils/criteria-manager.d.ts.map +1 -0
- package/dist/utils/criteria-manager.js +228 -0
- package/dist/utils/criteria-manager.js.map +1 -0
- package/dist/utils/errors.d.ts +27 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +67 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/helpers.d.ts +85 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +400 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/json-repair.d.ts +23 -0
- package/dist/utils/json-repair.d.ts.map +1 -0
- package/dist/utils/json-repair.js +208 -0
- package/dist/utils/json-repair.js.map +1 -0
- package/dist/utils/process-utils.d.ts +31 -0
- package/dist/utils/process-utils.d.ts.map +1 -0
- package/dist/utils/process-utils.js +217 -0
- package/dist/utils/process-utils.js.map +1 -0
- package/dist/utils/server-helpers.d.ts +4 -0
- package/dist/utils/server-helpers.d.ts.map +1 -0
- package/dist/utils/server-helpers.js +10 -0
- package/dist/utils/server-helpers.js.map +1 -0
- package/dist/utils/sse.d.ts +2 -0
- package/dist/utils/sse.d.ts.map +1 -0
- package/dist/utils/sse.js +6 -0
- package/dist/utils/sse.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1374 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { generateId, getCurrentTimestamp, getSafeEnvironment, sanitizeString, ensureDirectory, } from '../utils/helpers.js';
|
|
5
|
+
import { ExecutionError, TimeoutError, ResourceNotFoundError, ResourceLimitError, } from '../utils/errors.js';
|
|
6
|
+
import { StreamPublisher } from './stream-publisher.js';
|
|
7
|
+
import { FileStorageSubscriber } from './file-storage-subscriber.js';
|
|
8
|
+
import { StreamingPipelineReader } from './streaming-pipeline-reader.js';
|
|
9
|
+
import { RealtimeStreamSubscriber } from './realtime-stream-subscriber.js';
|
|
10
|
+
export class ProcessManager {
|
|
11
|
+
executions = new Map();
|
|
12
|
+
processes = new Map();
|
|
13
|
+
maxConcurrentProcesses;
|
|
14
|
+
outputDir;
|
|
15
|
+
terminalManager; // TerminalManager への参照
|
|
16
|
+
fileManager; // FileManager への参照
|
|
17
|
+
defaultWorkingDirectory;
|
|
18
|
+
allowedWorkingDirectories;
|
|
19
|
+
backgroundProcessCallbacks = {}; // バックグラウンドプロセス終了コールバック
|
|
20
|
+
// Issue #13: PUB/SUB統合 - Feature Flag付きで段階的統合
|
|
21
|
+
streamPublisher;
|
|
22
|
+
fileStorageSubscriber;
|
|
23
|
+
realtimeStreamSubscriber;
|
|
24
|
+
enableStreaming = false; // Feature Flag
|
|
25
|
+
constructor(maxConcurrentProcesses = 50, outputDir = '/tmp/mcp-shell-outputs', fileManager) {
|
|
26
|
+
this.maxConcurrentProcesses = maxConcurrentProcesses;
|
|
27
|
+
this.outputDir = outputDir;
|
|
28
|
+
this.fileManager = fileManager;
|
|
29
|
+
this.defaultWorkingDirectory = process.env['MCP_SHELL_DEFAULT_WORKDIR'] || process.cwd();
|
|
30
|
+
this.allowedWorkingDirectories = process.env['MCP_SHELL_ALLOWED_WORKDIRS']
|
|
31
|
+
? process.env['MCP_SHELL_ALLOWED_WORKDIRS'].split(',').map((dir) => dir.trim())
|
|
32
|
+
: [process.cwd()];
|
|
33
|
+
// StreamPublisher初期化
|
|
34
|
+
this.streamPublisher = new StreamPublisher({
|
|
35
|
+
enableRealtimeStreaming: false, // 初期状態は無効
|
|
36
|
+
bufferSize: 8192,
|
|
37
|
+
notificationInterval: 100,
|
|
38
|
+
});
|
|
39
|
+
// 環境変数でStreaming機能を制御(段階的展開、デフォルト有効)
|
|
40
|
+
this.enableStreaming = process.env['MCP_SHELL_ENABLE_STREAMING'] !== 'false';
|
|
41
|
+
if (this.enableStreaming) {
|
|
42
|
+
this.initializeStreamingComponents();
|
|
43
|
+
}
|
|
44
|
+
this.initializeOutputDirectory();
|
|
45
|
+
}
|
|
46
|
+
// TerminalManager への参照を設定
|
|
47
|
+
setTerminalManager(terminalManager) {
|
|
48
|
+
this.terminalManager = terminalManager;
|
|
49
|
+
}
|
|
50
|
+
// FileManager への参照を設定
|
|
51
|
+
setFileManager(fileManager) {
|
|
52
|
+
this.fileManager = fileManager;
|
|
53
|
+
// FileManagerが設定された時にStreaming機能を再初期化
|
|
54
|
+
if (this.enableStreaming) {
|
|
55
|
+
this.initializeStreamingComponents();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// バックグラウンドプロセス終了時のコールバックを設定
|
|
59
|
+
setBackgroundProcessCallbacks(callbacks) {
|
|
60
|
+
this.backgroundProcessCallbacks = callbacks;
|
|
61
|
+
}
|
|
62
|
+
// Issue #13: Streaming コンポーネントの初期化
|
|
63
|
+
initializeStreamingComponents() {
|
|
64
|
+
if (!this.fileManager) {
|
|
65
|
+
console.error('ProcessManager: FileManager is required for streaming components');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// FileStorageSubscriber初期化(既存FileManager機能を代替)
|
|
69
|
+
this.fileStorageSubscriber = new FileStorageSubscriber(this.fileManager, this.outputDir);
|
|
70
|
+
this.streamPublisher.subscribe(this.fileStorageSubscriber);
|
|
71
|
+
// RealtimeStreamSubscriber初期化
|
|
72
|
+
this.realtimeStreamSubscriber = new RealtimeStreamSubscriber({
|
|
73
|
+
bufferSize: 8192,
|
|
74
|
+
notificationInterval: 100,
|
|
75
|
+
maxRetentionSeconds: 3600,
|
|
76
|
+
maxBuffers: 1000,
|
|
77
|
+
});
|
|
78
|
+
this.streamPublisher.subscribe(this.realtimeStreamSubscriber);
|
|
79
|
+
console.error('ProcessManager: Streaming components initialized');
|
|
80
|
+
}
|
|
81
|
+
// Issue #13: Streaming機能の有効/無効切り替え
|
|
82
|
+
enableStreamingFeature(enable = true) {
|
|
83
|
+
this.enableStreaming = enable;
|
|
84
|
+
if (enable && this.fileManager) {
|
|
85
|
+
this.initializeStreamingComponents();
|
|
86
|
+
}
|
|
87
|
+
else if (!enable) {
|
|
88
|
+
// Streaming無効化時のクリーンアップ
|
|
89
|
+
if (this.realtimeStreamSubscriber) {
|
|
90
|
+
this.streamPublisher.unsubscribe(this.realtimeStreamSubscriber.id);
|
|
91
|
+
this.realtimeStreamSubscriber.destroy();
|
|
92
|
+
this.realtimeStreamSubscriber = undefined;
|
|
93
|
+
}
|
|
94
|
+
if (this.fileStorageSubscriber) {
|
|
95
|
+
this.streamPublisher.unsubscribe(this.fileStorageSubscriber.id);
|
|
96
|
+
this.fileStorageSubscriber = undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Issue #13: RealtimeStreamSubscriber への参照を取得(新しいMCPツール用)
|
|
101
|
+
getRealtimeStreamSubscriber() {
|
|
102
|
+
return this.realtimeStreamSubscriber;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Issue #13: output_idから実行IDを取得
|
|
106
|
+
*/
|
|
107
|
+
findExecutionIdByOutputId(outputId) {
|
|
108
|
+
return this.fileManager?.getExecutionIdByOutputId(outputId);
|
|
109
|
+
}
|
|
110
|
+
async initializeOutputDirectory() {
|
|
111
|
+
await ensureDirectory(this.outputDir);
|
|
112
|
+
}
|
|
113
|
+
async executeCommand(options) {
|
|
114
|
+
// 同時実行数のチェック
|
|
115
|
+
const runningProcesses = Array.from(this.executions.values()).filter((exec) => exec.status === 'running').length;
|
|
116
|
+
if (runningProcesses >= this.maxConcurrentProcesses) {
|
|
117
|
+
throw new ResourceLimitError('concurrent processes', this.maxConcurrentProcesses);
|
|
118
|
+
}
|
|
119
|
+
// 入力データの準備 - input_output_idが指定された場合の処理
|
|
120
|
+
let resolvedInputData = options.inputData;
|
|
121
|
+
let inputStream = undefined;
|
|
122
|
+
if (options.inputOutputId) {
|
|
123
|
+
if (!this.fileManager) {
|
|
124
|
+
throw new ExecutionError('FileManager is not available for input_output_id processing', {
|
|
125
|
+
inputOutputId: options.inputOutputId,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// output_idから実行IDを特定
|
|
129
|
+
const sourceExecutionId = this.findExecutionIdByOutputId(options.inputOutputId);
|
|
130
|
+
if (sourceExecutionId && this.realtimeStreamSubscriber) {
|
|
131
|
+
// 実行中プロセスの場合: StreamingPipelineReaderを使用
|
|
132
|
+
const streamState = this.realtimeStreamSubscriber.getStreamState(sourceExecutionId);
|
|
133
|
+
if (streamState && streamState.isActive) {
|
|
134
|
+
console.error(`ProcessManager: Using streaming pipeline for active process ${sourceExecutionId}`);
|
|
135
|
+
inputStream = new StreamingPipelineReader(this.fileManager, this.realtimeStreamSubscriber, options.inputOutputId, sourceExecutionId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 実行中プロセスでない場合、または失敗した場合: 従来のファイル読み取り
|
|
139
|
+
if (!inputStream) {
|
|
140
|
+
try {
|
|
141
|
+
console.error(`ProcessManager: Using traditional file read for ${options.inputOutputId}`);
|
|
142
|
+
const result = await this.fileManager.readFile(options.inputOutputId, 0, 100 * 1024 * 1024, // 100MB まで読み取り
|
|
143
|
+
'utf-8');
|
|
144
|
+
resolvedInputData = result.content;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new ExecutionError(`Failed to read input from output_id: ${options.inputOutputId}`, {
|
|
148
|
+
inputOutputId: options.inputOutputId,
|
|
149
|
+
originalError: String(error),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const executionId = generateId();
|
|
155
|
+
const startTime = getCurrentTimestamp();
|
|
156
|
+
// 実行情報の初期化
|
|
157
|
+
const resolvedWorkingDirectory = this.resolveWorkingDirectory(options.workingDirectory);
|
|
158
|
+
const executionInfo = {
|
|
159
|
+
execution_id: executionId,
|
|
160
|
+
command: options.command,
|
|
161
|
+
status: 'running',
|
|
162
|
+
working_directory: resolvedWorkingDirectory,
|
|
163
|
+
default_working_directory: this.defaultWorkingDirectory,
|
|
164
|
+
working_directory_changed: resolvedWorkingDirectory !== this.defaultWorkingDirectory,
|
|
165
|
+
created_at: startTime,
|
|
166
|
+
started_at: startTime,
|
|
167
|
+
};
|
|
168
|
+
if (options.environmentVariables) {
|
|
169
|
+
executionInfo.environment_variables = options.environmentVariables;
|
|
170
|
+
}
|
|
171
|
+
this.executions.set(executionId, executionInfo);
|
|
172
|
+
// 新規ターミナル作成オプションがある場合
|
|
173
|
+
if (options.createTerminal && this.terminalManager) {
|
|
174
|
+
try {
|
|
175
|
+
const terminalOptions = {
|
|
176
|
+
sessionName: `exec-${executionId}`,
|
|
177
|
+
shellType: options.terminalShell || 'bash',
|
|
178
|
+
dimensions: options.terminalDimensions || { width: 80, height: 24 },
|
|
179
|
+
autoSaveHistory: true,
|
|
180
|
+
};
|
|
181
|
+
if (options.workingDirectory) {
|
|
182
|
+
terminalOptions.workingDirectory = options.workingDirectory;
|
|
183
|
+
}
|
|
184
|
+
if (options.environmentVariables) {
|
|
185
|
+
terminalOptions.environmentVariables = options.environmentVariables;
|
|
186
|
+
}
|
|
187
|
+
const terminalInfo = await this.terminalManager.createTerminal(terminalOptions);
|
|
188
|
+
executionInfo.terminal_id = terminalInfo.terminal_id;
|
|
189
|
+
// ターミナルにコマンドを送信
|
|
190
|
+
this.terminalManager.sendInput(terminalInfo.terminal_id, options.command, true);
|
|
191
|
+
// 実行情報を更新
|
|
192
|
+
executionInfo.status = 'completed';
|
|
193
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
194
|
+
this.executions.set(executionId, executionInfo);
|
|
195
|
+
return executionInfo;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
executionInfo.status = 'failed';
|
|
199
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
200
|
+
this.executions.set(executionId, executionInfo);
|
|
201
|
+
throw new ExecutionError(`Failed to create terminal: ${error}`, {
|
|
202
|
+
originalError: String(error),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
// 実行オプションを準備
|
|
208
|
+
const { inputOutputId: _inputOutputId, ...baseOptions } = options;
|
|
209
|
+
const updatedOptions = {
|
|
210
|
+
...baseOptions,
|
|
211
|
+
...(resolvedInputData !== undefined && { inputData: resolvedInputData }),
|
|
212
|
+
};
|
|
213
|
+
// StreamingPipelineReaderがある場合は特別処理
|
|
214
|
+
if (inputStream) {
|
|
215
|
+
return await this.executeCommandWithInputStream(executionId, updatedOptions, inputStream);
|
|
216
|
+
}
|
|
217
|
+
switch (options.executionMode) {
|
|
218
|
+
case 'foreground':
|
|
219
|
+
return await this.executeForegroundCommand(executionId, updatedOptions);
|
|
220
|
+
case 'adaptive':
|
|
221
|
+
return await this.executeAdaptiveCommand(executionId, updatedOptions);
|
|
222
|
+
case 'background':
|
|
223
|
+
return await this.executeBackgroundCommand(executionId, updatedOptions);
|
|
224
|
+
case 'detached':
|
|
225
|
+
return await this.executeDetachedCommand(executionId, updatedOptions);
|
|
226
|
+
default:
|
|
227
|
+
throw new ExecutionError('Unsupported execution mode', { mode: options.executionMode });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
// エラー時の実行情報更新
|
|
232
|
+
const updatedInfo = this.executions.get(executionId);
|
|
233
|
+
if (updatedInfo) {
|
|
234
|
+
updatedInfo.status = 'failed';
|
|
235
|
+
updatedInfo.completed_at = getCurrentTimestamp();
|
|
236
|
+
this.executions.set(executionId, updatedInfo);
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Issue #13: StreamingPipelineReaderを使用したコマンド実行
|
|
243
|
+
*/
|
|
244
|
+
async executeCommandWithInputStream(executionId, options, inputStream) {
|
|
245
|
+
console.error(`ProcessManager: Executing command with input stream for ${executionId}`);
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
const startTime = Date.now();
|
|
248
|
+
let stdout = '';
|
|
249
|
+
let stderr = '';
|
|
250
|
+
let outputTruncated = false;
|
|
251
|
+
// 環境変数の準備
|
|
252
|
+
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
253
|
+
// プロセスの起動
|
|
254
|
+
const child = spawn('sh', ['-c', options.command], {
|
|
255
|
+
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
256
|
+
env,
|
|
257
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
258
|
+
});
|
|
259
|
+
// StreamingPipelineReaderをSTDINに接続
|
|
260
|
+
if (child.stdin) {
|
|
261
|
+
inputStream.pipe(child.stdin);
|
|
262
|
+
}
|
|
263
|
+
inputStream.on('error', (error) => {
|
|
264
|
+
console.error(`StreamingPipelineReader error for ${executionId}: ${error.message}`);
|
|
265
|
+
child.kill('SIGTERM');
|
|
266
|
+
});
|
|
267
|
+
// StreamPublisher通知
|
|
268
|
+
if (this.streamPublisher) {
|
|
269
|
+
this.streamPublisher.notifyProcessStart(executionId, options.command);
|
|
270
|
+
}
|
|
271
|
+
// STDOUT処理
|
|
272
|
+
if (child.stdout) {
|
|
273
|
+
child.stdout.on('data', (data) => {
|
|
274
|
+
const chunk = data.toString();
|
|
275
|
+
if (stdout.length + chunk.length <= options.maxOutputSize) {
|
|
276
|
+
stdout += chunk;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
outputTruncated = true;
|
|
280
|
+
}
|
|
281
|
+
// StreamPublisher通知
|
|
282
|
+
if (this.streamPublisher) {
|
|
283
|
+
this.streamPublisher.notifyOutputData(executionId, chunk, false);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// STDERR処理
|
|
288
|
+
if (options.captureStderr && child.stderr) {
|
|
289
|
+
child.stderr.on('data', (data) => {
|
|
290
|
+
const chunk = data.toString();
|
|
291
|
+
if (stderr.length + chunk.length <= options.maxOutputSize) {
|
|
292
|
+
stderr += chunk;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
outputTruncated = true;
|
|
296
|
+
}
|
|
297
|
+
// StreamPublisher通知
|
|
298
|
+
if (this.streamPublisher) {
|
|
299
|
+
this.streamPublisher.notifyOutputData(executionId, chunk, true);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// プロセス終了処理
|
|
304
|
+
child.on('close', async (code) => {
|
|
305
|
+
const executionInfo = this.executions.get(executionId);
|
|
306
|
+
if (!executionInfo) {
|
|
307
|
+
reject(new ExecutionError('Execution info not found', { executionId }));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// 実行時間の計算
|
|
311
|
+
const executionTime = Date.now() - startTime;
|
|
312
|
+
// 実行情報の更新
|
|
313
|
+
executionInfo.status = code === 0 ? 'completed' : 'failed';
|
|
314
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
315
|
+
if (code !== null) {
|
|
316
|
+
executionInfo.exit_code = code;
|
|
317
|
+
}
|
|
318
|
+
executionInfo.execution_time_ms = executionTime;
|
|
319
|
+
// 出力の保存
|
|
320
|
+
if (this.fileManager) {
|
|
321
|
+
try {
|
|
322
|
+
const combinedOutput = stdout + (options.captureStderr ? stderr : '');
|
|
323
|
+
if (combinedOutput) {
|
|
324
|
+
const outputId = await this.fileManager.createOutputFile(combinedOutput, executionId);
|
|
325
|
+
executionInfo.output_id = outputId;
|
|
326
|
+
executionInfo.output_truncated = outputTruncated;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
console.error(`Failed to save output for ${executionId}: ${error}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
this.executions.set(executionId, executionInfo);
|
|
334
|
+
// StreamPublisher通知
|
|
335
|
+
if (this.streamPublisher) {
|
|
336
|
+
this.streamPublisher.notifyProcessEnd(executionId, code);
|
|
337
|
+
}
|
|
338
|
+
console.error(`Command completed: ${options.command} (exit code: ${code})`);
|
|
339
|
+
resolve(executionInfo);
|
|
340
|
+
});
|
|
341
|
+
child.on('error', (error) => {
|
|
342
|
+
console.error(`Process error for ${executionId}: ${error.message}`);
|
|
343
|
+
// StreamPublisher通知
|
|
344
|
+
if (this.streamPublisher) {
|
|
345
|
+
this.streamPublisher.notifyError(executionId, error);
|
|
346
|
+
}
|
|
347
|
+
reject(new ExecutionError(`Process error: ${error.message}`, { originalError: String(error) }));
|
|
348
|
+
});
|
|
349
|
+
// タイムアウト処理
|
|
350
|
+
const timeout = setTimeout(() => {
|
|
351
|
+
console.error(`Process timeout for ${executionId}`);
|
|
352
|
+
child.kill('SIGTERM');
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
if (!child.killed) {
|
|
355
|
+
child.kill('SIGKILL');
|
|
356
|
+
}
|
|
357
|
+
}, 5000);
|
|
358
|
+
}, options.timeoutSeconds * 1000);
|
|
359
|
+
child.on('close', () => {
|
|
360
|
+
clearTimeout(timeout);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
async executeForegroundCommand(executionId, options) {
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
const startTime = Date.now();
|
|
367
|
+
let stdout = '';
|
|
368
|
+
let stderr = '';
|
|
369
|
+
let outputTruncated = false;
|
|
370
|
+
// 環境変数の準備
|
|
371
|
+
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
372
|
+
// プロセスの起動
|
|
373
|
+
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
374
|
+
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
375
|
+
env,
|
|
376
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
377
|
+
});
|
|
378
|
+
if (childProcess.pid) {
|
|
379
|
+
this.processes.set(childProcess.pid, childProcess);
|
|
380
|
+
}
|
|
381
|
+
// タイムアウトの設定
|
|
382
|
+
const timeout = setTimeout(async () => {
|
|
383
|
+
childProcess.kill('SIGTERM');
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
if (!childProcess.killed) {
|
|
386
|
+
childProcess.kill('SIGKILL');
|
|
387
|
+
}
|
|
388
|
+
}, 5000);
|
|
389
|
+
const executionTime = Date.now() - startTime;
|
|
390
|
+
const executionInfo = this.executions.get(executionId);
|
|
391
|
+
if (executionInfo) {
|
|
392
|
+
executionInfo.status = 'timeout';
|
|
393
|
+
executionInfo.stdout = sanitizeString(stdout);
|
|
394
|
+
executionInfo.stderr = sanitizeString(stderr);
|
|
395
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
396
|
+
executionInfo.execution_time_ms = executionTime;
|
|
397
|
+
if (childProcess.pid !== undefined) {
|
|
398
|
+
executionInfo.process_id = childProcess.pid;
|
|
399
|
+
}
|
|
400
|
+
// 出力をFileManagerに保存(サイズに関係なく)
|
|
401
|
+
let outputFileId;
|
|
402
|
+
try {
|
|
403
|
+
outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
404
|
+
executionInfo.output_id = outputFileId;
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
// ファイル保存失敗は重要なエラーとしてログに記録し、実行情報に含める
|
|
408
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
409
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
410
|
+
}
|
|
411
|
+
// 出力状態の詳細情報を設定
|
|
412
|
+
this.setOutputStatus(executionInfo, outputTruncated, 'timeout', outputFileId);
|
|
413
|
+
this.executions.set(executionId, executionInfo);
|
|
414
|
+
// return_partial_on_timeout が true の場合は部分結果を返す
|
|
415
|
+
if (options.returnPartialOnTimeout) {
|
|
416
|
+
resolve(executionInfo);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
reject(new TimeoutError(options.timeoutSeconds));
|
|
421
|
+
}, options.timeoutSeconds * 1000);
|
|
422
|
+
// 標準入力の送信
|
|
423
|
+
if (options.inputData) {
|
|
424
|
+
childProcess.stdin?.write(options.inputData);
|
|
425
|
+
childProcess.stdin?.end();
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
childProcess.stdin?.end();
|
|
429
|
+
}
|
|
430
|
+
// 標準出力の処理
|
|
431
|
+
childProcess.stdout?.on('data', (data) => {
|
|
432
|
+
const output = data.toString();
|
|
433
|
+
if (stdout.length + output.length <= options.maxOutputSize) {
|
|
434
|
+
stdout += output;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
stdout += output.substring(0, options.maxOutputSize - stdout.length);
|
|
438
|
+
outputTruncated = true;
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
// 標準エラー出力の処理
|
|
442
|
+
if (options.captureStderr) {
|
|
443
|
+
childProcess.stderr?.on('data', (data) => {
|
|
444
|
+
const output = data.toString();
|
|
445
|
+
if (stderr.length + output.length <= options.maxOutputSize) {
|
|
446
|
+
stderr += output;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
stderr += output.substring(0, options.maxOutputSize - stderr.length);
|
|
450
|
+
outputTruncated = true;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
// プロセス終了時の処理
|
|
455
|
+
childProcess.on('close', async (code) => {
|
|
456
|
+
clearTimeout(timeout);
|
|
457
|
+
if (childProcess.pid) {
|
|
458
|
+
this.processes.delete(childProcess.pid);
|
|
459
|
+
}
|
|
460
|
+
const executionTime = Date.now() - startTime;
|
|
461
|
+
const executionInfo = this.executions.get(executionId);
|
|
462
|
+
if (executionInfo) {
|
|
463
|
+
executionInfo.status = 'completed';
|
|
464
|
+
executionInfo.exit_code = code || 0;
|
|
465
|
+
executionInfo.stdout = sanitizeString(stdout);
|
|
466
|
+
executionInfo.stderr = sanitizeString(stderr);
|
|
467
|
+
executionInfo.execution_time_ms = executionTime;
|
|
468
|
+
if (childProcess.pid !== undefined) {
|
|
469
|
+
executionInfo.process_id = childProcess.pid;
|
|
470
|
+
}
|
|
471
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
472
|
+
// 出力をFileManagerに保存(サイズに関係なく)
|
|
473
|
+
let outputFileId;
|
|
474
|
+
try {
|
|
475
|
+
outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
476
|
+
executionInfo.output_id = outputFileId;
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
// ファイル保存失敗は重要なエラーとしてログに記録し、実行情報に含める
|
|
480
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
481
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
482
|
+
}
|
|
483
|
+
// 出力状態の詳細情報を設定
|
|
484
|
+
if (outputTruncated) {
|
|
485
|
+
this.setOutputStatus(executionInfo, true, 'size_limit', outputFileId);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
// 通常完了時 - actuallyTruncated=false, 適当なreasonで完了時ガイダンスを表示
|
|
489
|
+
this.setOutputStatus(executionInfo, false, 'size_limit', outputFileId);
|
|
490
|
+
}
|
|
491
|
+
this.executions.set(executionId, executionInfo);
|
|
492
|
+
resolve(executionInfo);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
// エラー処理
|
|
496
|
+
childProcess.on('error', (error) => {
|
|
497
|
+
clearTimeout(timeout);
|
|
498
|
+
if (childProcess.pid) {
|
|
499
|
+
this.processes.delete(childProcess.pid);
|
|
500
|
+
}
|
|
501
|
+
const executionInfo = this.executions.get(executionId);
|
|
502
|
+
if (executionInfo) {
|
|
503
|
+
executionInfo.status = 'failed';
|
|
504
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
505
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
506
|
+
this.executions.set(executionId, executionInfo);
|
|
507
|
+
}
|
|
508
|
+
reject(new ExecutionError(`Process execution failed: ${error.message}`, {
|
|
509
|
+
originalError: error.message,
|
|
510
|
+
}));
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async executeAdaptiveCommand(executionId, options) {
|
|
515
|
+
// adaptiveモード: 1つのプロセスを起動し、以下の条件でバックグラウンドに移行
|
|
516
|
+
// 1. フォアグラウンドタイムアウトに達した場合
|
|
517
|
+
// 2. 出力サイズ制限に達した場合
|
|
518
|
+
const returnPartialOnTimeout = options.returnPartialOnTimeout ?? true;
|
|
519
|
+
const foregroundTimeout = options.foregroundTimeoutSeconds ?? 10;
|
|
520
|
+
return new Promise((resolve, reject) => {
|
|
521
|
+
const startTime = Date.now();
|
|
522
|
+
let stdout = '';
|
|
523
|
+
let stderr = '';
|
|
524
|
+
let outputTruncated = false;
|
|
525
|
+
let backgroundTransitionReason = null;
|
|
526
|
+
// 環境変数の準備
|
|
527
|
+
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
528
|
+
// プロセスの起動(バックグラウンド対応)
|
|
529
|
+
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
530
|
+
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
531
|
+
env,
|
|
532
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
533
|
+
});
|
|
534
|
+
if (childProcess.pid) {
|
|
535
|
+
this.processes.set(childProcess.pid, childProcess);
|
|
536
|
+
}
|
|
537
|
+
// フォアグラウンドタイムアウトの設定
|
|
538
|
+
const foregroundTimeoutHandle = setTimeout(() => {
|
|
539
|
+
if (!backgroundTransitionReason) {
|
|
540
|
+
backgroundTransitionReason = 'timeout';
|
|
541
|
+
transitionToBackground();
|
|
542
|
+
}
|
|
543
|
+
}, foregroundTimeout * 1000);
|
|
544
|
+
// 最終タイムアウトの設定
|
|
545
|
+
const finalTimeoutHandle = setTimeout(async () => {
|
|
546
|
+
childProcess.kill('SIGTERM');
|
|
547
|
+
setTimeout(() => {
|
|
548
|
+
if (!childProcess.killed) {
|
|
549
|
+
childProcess.kill('SIGKILL');
|
|
550
|
+
}
|
|
551
|
+
}, 5000);
|
|
552
|
+
const executionInfo = this.executions.get(executionId);
|
|
553
|
+
if (executionInfo) {
|
|
554
|
+
executionInfo.status = 'timeout';
|
|
555
|
+
executionInfo.stdout = sanitizeString(stdout);
|
|
556
|
+
executionInfo.stderr = sanitizeString(stderr);
|
|
557
|
+
executionInfo.output_truncated = outputTruncated;
|
|
558
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
559
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
560
|
+
// 出力をFileManagerに保存
|
|
561
|
+
try {
|
|
562
|
+
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
563
|
+
executionInfo.output_id = outputFileId;
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
// ファイル保存失敗は重要なエラーとしてログに記録し、実行情報に含める
|
|
567
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
568
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
569
|
+
}
|
|
570
|
+
this.executions.set(executionId, executionInfo);
|
|
571
|
+
if (returnPartialOnTimeout) {
|
|
572
|
+
resolve(executionInfo);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
reject(new TimeoutError(options.timeoutSeconds));
|
|
577
|
+
}, options.timeoutSeconds * 1000);
|
|
578
|
+
// バックグラウンドに移行する関数
|
|
579
|
+
const transitionToBackground = async () => {
|
|
580
|
+
clearTimeout(foregroundTimeoutHandle);
|
|
581
|
+
const executionInfo = this.executions.get(executionId);
|
|
582
|
+
if (executionInfo) {
|
|
583
|
+
executionInfo.status = 'running';
|
|
584
|
+
executionInfo.stdout = sanitizeString(stdout);
|
|
585
|
+
executionInfo.stderr = sanitizeString(stderr);
|
|
586
|
+
// 移行理由を記録
|
|
587
|
+
if (backgroundTransitionReason === 'timeout') {
|
|
588
|
+
executionInfo.transition_reason = 'foreground_timeout';
|
|
589
|
+
}
|
|
590
|
+
else if (backgroundTransitionReason === 'output_size_limit') {
|
|
591
|
+
executionInfo.transition_reason = 'output_size_limit';
|
|
592
|
+
}
|
|
593
|
+
if (childProcess.pid !== undefined) {
|
|
594
|
+
executionInfo.process_id = childProcess.pid;
|
|
595
|
+
}
|
|
596
|
+
// 出力をFileManagerに保存
|
|
597
|
+
let outputFileId;
|
|
598
|
+
try {
|
|
599
|
+
outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
600
|
+
executionInfo.output_id = outputFileId;
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
// ファイル保存失敗は重要なエラーとしてログに記録し、実行情報に含める
|
|
604
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
605
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
606
|
+
}
|
|
607
|
+
// 出力状態の詳細情報を設定(バックグラウンド移行)
|
|
608
|
+
this.setOutputStatus(executionInfo, outputTruncated, 'background_transition', outputFileId);
|
|
609
|
+
this.executions.set(executionId, executionInfo);
|
|
610
|
+
// バックグラウンド処理の継続設定(adaptive mode専用)
|
|
611
|
+
this.handleAdaptiveBackgroundTransition(executionId, childProcess, {
|
|
612
|
+
...options,
|
|
613
|
+
timeoutSeconds: Math.max(1, options.timeoutSeconds - Math.floor((Date.now() - startTime) / 1000)),
|
|
614
|
+
});
|
|
615
|
+
resolve(executionInfo);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
// 標準入力の送信
|
|
619
|
+
if (options.inputData) {
|
|
620
|
+
childProcess.stdin?.write(options.inputData);
|
|
621
|
+
childProcess.stdin?.end();
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
childProcess.stdin?.end();
|
|
625
|
+
}
|
|
626
|
+
// 標準出力の処理
|
|
627
|
+
childProcess.stdout?.on('data', (data) => {
|
|
628
|
+
const output = data.toString();
|
|
629
|
+
if (stdout.length + output.length <= options.maxOutputSize) {
|
|
630
|
+
stdout += output;
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
stdout += output.substring(0, options.maxOutputSize - stdout.length);
|
|
634
|
+
outputTruncated = true;
|
|
635
|
+
// 出力サイズ制限に達した場合、バックグラウンドに移行
|
|
636
|
+
if (!backgroundTransitionReason) {
|
|
637
|
+
backgroundTransitionReason = 'output_size_limit';
|
|
638
|
+
transitionToBackground();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
// 標準エラー出力の処理
|
|
643
|
+
if (options.captureStderr) {
|
|
644
|
+
childProcess.stderr?.on('data', (data) => {
|
|
645
|
+
const output = data.toString();
|
|
646
|
+
if (stderr.length + output.length <= options.maxOutputSize) {
|
|
647
|
+
stderr += output;
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
stderr += output.substring(0, options.maxOutputSize - stderr.length);
|
|
651
|
+
outputTruncated = true;
|
|
652
|
+
// 出力サイズ制限に達した場合、バックグラウンドに移行
|
|
653
|
+
if (!backgroundTransitionReason) {
|
|
654
|
+
backgroundTransitionReason = 'output_size_limit';
|
|
655
|
+
transitionToBackground();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
// プロセス終了時の処理
|
|
661
|
+
childProcess.on('close', async (code) => {
|
|
662
|
+
clearTimeout(foregroundTimeoutHandle);
|
|
663
|
+
clearTimeout(finalTimeoutHandle);
|
|
664
|
+
if (childProcess.pid) {
|
|
665
|
+
this.processes.delete(childProcess.pid);
|
|
666
|
+
}
|
|
667
|
+
// バックグラウンドに移行していない場合のみ処理
|
|
668
|
+
if (!backgroundTransitionReason) {
|
|
669
|
+
const executionTime = Date.now() - startTime;
|
|
670
|
+
const executionInfo = this.executions.get(executionId);
|
|
671
|
+
if (executionInfo) {
|
|
672
|
+
executionInfo.status = 'completed';
|
|
673
|
+
executionInfo.exit_code = code || 0;
|
|
674
|
+
executionInfo.stdout = sanitizeString(stdout);
|
|
675
|
+
executionInfo.stderr = sanitizeString(stderr);
|
|
676
|
+
executionInfo.output_truncated = outputTruncated;
|
|
677
|
+
executionInfo.execution_time_ms = executionTime;
|
|
678
|
+
if (childProcess.pid !== undefined) {
|
|
679
|
+
executionInfo.process_id = childProcess.pid;
|
|
680
|
+
}
|
|
681
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
682
|
+
// 出力をFileManagerに保存
|
|
683
|
+
try {
|
|
684
|
+
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
685
|
+
executionInfo.output_id = outputFileId;
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
// ファイル保存失敗は重要なエラーとしてログに記録し、実行情報に含める
|
|
689
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
690
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
691
|
+
}
|
|
692
|
+
this.executions.set(executionId, executionInfo);
|
|
693
|
+
resolve(executionInfo);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
// エラー処理
|
|
698
|
+
childProcess.on('error', (error) => {
|
|
699
|
+
clearTimeout(foregroundTimeoutHandle);
|
|
700
|
+
clearTimeout(finalTimeoutHandle);
|
|
701
|
+
if (childProcess.pid) {
|
|
702
|
+
this.processes.delete(childProcess.pid);
|
|
703
|
+
}
|
|
704
|
+
if (!backgroundTransitionReason) {
|
|
705
|
+
const executionInfo = this.executions.get(executionId);
|
|
706
|
+
if (executionInfo) {
|
|
707
|
+
executionInfo.status = 'failed';
|
|
708
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
709
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
710
|
+
this.executions.set(executionId, executionInfo);
|
|
711
|
+
}
|
|
712
|
+
reject(new ExecutionError(`Process execution failed: ${error.message}`, {
|
|
713
|
+
originalError: error.message,
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
async executeBackgroundCommand(executionId, options) {
|
|
720
|
+
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
721
|
+
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
722
|
+
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
723
|
+
env,
|
|
724
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
725
|
+
detached: options.executionMode === 'background',
|
|
726
|
+
});
|
|
727
|
+
if (childProcess.pid) {
|
|
728
|
+
this.processes.set(childProcess.pid, childProcess);
|
|
729
|
+
}
|
|
730
|
+
const executionInfo = this.executions.get(executionId);
|
|
731
|
+
if (executionInfo && childProcess.pid !== undefined) {
|
|
732
|
+
executionInfo.process_id = childProcess.pid;
|
|
733
|
+
this.executions.set(executionId, executionInfo);
|
|
734
|
+
}
|
|
735
|
+
// バックグラウンドプロセスの場合、出力を非同期で処理
|
|
736
|
+
if (options.executionMode === 'background') {
|
|
737
|
+
this.handleBackgroundProcess(executionId, childProcess, options);
|
|
738
|
+
}
|
|
739
|
+
const resultExecutionInfo = this.executions.get(executionId);
|
|
740
|
+
if (!resultExecutionInfo) {
|
|
741
|
+
throw new Error(`Execution info not found for ID: ${executionId}`);
|
|
742
|
+
}
|
|
743
|
+
return resultExecutionInfo;
|
|
744
|
+
}
|
|
745
|
+
handleBackgroundProcess(executionId, childProcess, options) {
|
|
746
|
+
const startTime = Date.now();
|
|
747
|
+
let stdout = '';
|
|
748
|
+
let stderr = '';
|
|
749
|
+
// タイムアウトの設定(backgroundプロセス用)
|
|
750
|
+
const timeout = setTimeout(async () => {
|
|
751
|
+
childProcess.kill('SIGTERM');
|
|
752
|
+
setTimeout(() => {
|
|
753
|
+
if (!childProcess.killed) {
|
|
754
|
+
childProcess.kill('SIGKILL');
|
|
755
|
+
}
|
|
756
|
+
}, 5000);
|
|
757
|
+
const executionInfo = this.executions.get(executionId);
|
|
758
|
+
if (executionInfo) {
|
|
759
|
+
executionInfo.status = 'timeout';
|
|
760
|
+
executionInfo.stdout = stdout;
|
|
761
|
+
executionInfo.stderr = stderr;
|
|
762
|
+
executionInfo.output_truncated = true;
|
|
763
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
764
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
765
|
+
// 出力をFileManagerに保存
|
|
766
|
+
try {
|
|
767
|
+
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
768
|
+
executionInfo.output_id = outputFileId;
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
// ファイル保存失敗は重要なエラーとしてログに記録し、実行情報に含める
|
|
772
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
773
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
774
|
+
}
|
|
775
|
+
this.executions.set(executionId, executionInfo);
|
|
776
|
+
// バックグラウンドプロセスタイムアウトのコールバック呼び出し
|
|
777
|
+
if (this.backgroundProcessCallbacks.onTimeout) {
|
|
778
|
+
setImmediate(async () => {
|
|
779
|
+
try {
|
|
780
|
+
const callback = this.backgroundProcessCallbacks.onTimeout;
|
|
781
|
+
if (callback) {
|
|
782
|
+
const result = callback(executionId, executionInfo);
|
|
783
|
+
if (result instanceof Promise) {
|
|
784
|
+
await result;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
catch (callbackError) {
|
|
789
|
+
// コールバックエラーは内部ログに記録のみ
|
|
790
|
+
// console.error('Background process timeout callback error:', callbackError);
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}, options.timeoutSeconds * 1000);
|
|
796
|
+
// 出力の収集
|
|
797
|
+
childProcess.stdout?.on('data', (data) => {
|
|
798
|
+
stdout += data.toString();
|
|
799
|
+
});
|
|
800
|
+
if (options.captureStderr) {
|
|
801
|
+
childProcess.stderr?.on('data', (data) => {
|
|
802
|
+
stderr += data.toString();
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
// プロセス終了時の処理
|
|
806
|
+
childProcess.on('close', async (code) => {
|
|
807
|
+
clearTimeout(timeout);
|
|
808
|
+
if (childProcess.pid) {
|
|
809
|
+
this.processes.delete(childProcess.pid);
|
|
810
|
+
}
|
|
811
|
+
const executionInfo = this.executions.get(executionId);
|
|
812
|
+
if (executionInfo) {
|
|
813
|
+
executionInfo.status = 'completed';
|
|
814
|
+
executionInfo.exit_code = code || 0;
|
|
815
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
816
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
817
|
+
// 出力をファイルに保存
|
|
818
|
+
try {
|
|
819
|
+
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
820
|
+
executionInfo.output_id = outputFileId;
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
// ファイル保存失敗は重要なエラーとしてログに記録し、実行情報に含める
|
|
824
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
825
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
826
|
+
}
|
|
827
|
+
this.executions.set(executionId, executionInfo);
|
|
828
|
+
// バックグラウンドプロセス正常終了のコールバック呼び出し
|
|
829
|
+
if (this.backgroundProcessCallbacks.onComplete) {
|
|
830
|
+
setImmediate(async () => {
|
|
831
|
+
try {
|
|
832
|
+
const callback = this.backgroundProcessCallbacks.onComplete;
|
|
833
|
+
if (callback) {
|
|
834
|
+
const result = callback(executionId, executionInfo);
|
|
835
|
+
if (result instanceof Promise) {
|
|
836
|
+
await result;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch (callbackError) {
|
|
841
|
+
// コールバックエラーは内部ログに記録のみ
|
|
842
|
+
// console.error('Background process completion callback error:', callbackError);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
childProcess.on('error', (error) => {
|
|
849
|
+
clearTimeout(timeout);
|
|
850
|
+
if (childProcess.pid) {
|
|
851
|
+
this.processes.delete(childProcess.pid);
|
|
852
|
+
}
|
|
853
|
+
const executionInfo = this.executions.get(executionId);
|
|
854
|
+
if (executionInfo) {
|
|
855
|
+
executionInfo.status = 'failed';
|
|
856
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
857
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
858
|
+
this.executions.set(executionId, executionInfo);
|
|
859
|
+
// バックグラウンドプロセスエラーのコールバック呼び出し
|
|
860
|
+
if (this.backgroundProcessCallbacks.onError) {
|
|
861
|
+
setImmediate(async () => {
|
|
862
|
+
try {
|
|
863
|
+
const callback = this.backgroundProcessCallbacks.onError;
|
|
864
|
+
if (callback) {
|
|
865
|
+
const result = callback(executionId, executionInfo, error);
|
|
866
|
+
if (result instanceof Promise) {
|
|
867
|
+
await result;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
catch (callbackError) {
|
|
872
|
+
// コールバックエラーは内部ログに記録のみ
|
|
873
|
+
// console.error('Background process error callback error:', callbackError);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
// adaptive modeでバックグラウンドに移行したプロセスの処理
|
|
881
|
+
handleAdaptiveBackgroundTransition(executionId, childProcess, options) {
|
|
882
|
+
// タイムアウトの設定(最終タイムアウト)
|
|
883
|
+
const timeout = setTimeout(async () => {
|
|
884
|
+
childProcess.kill('SIGTERM');
|
|
885
|
+
setTimeout(() => {
|
|
886
|
+
if (!childProcess.killed) {
|
|
887
|
+
childProcess.kill('SIGKILL');
|
|
888
|
+
}
|
|
889
|
+
}, 5000);
|
|
890
|
+
const executionInfo = this.executions.get(executionId);
|
|
891
|
+
if (executionInfo) {
|
|
892
|
+
executionInfo.status = 'timeout';
|
|
893
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
894
|
+
// 既存の出力は保持(adaptive modeで既にキャプチャ済み)
|
|
895
|
+
this.executions.set(executionId, executionInfo);
|
|
896
|
+
}
|
|
897
|
+
}, options.timeoutSeconds * 1000);
|
|
898
|
+
// プロセス終了時の処理
|
|
899
|
+
childProcess.on('close', async (code) => {
|
|
900
|
+
clearTimeout(timeout);
|
|
901
|
+
if (childProcess.pid) {
|
|
902
|
+
this.processes.delete(childProcess.pid);
|
|
903
|
+
}
|
|
904
|
+
const executionInfo = this.executions.get(executionId);
|
|
905
|
+
if (executionInfo) {
|
|
906
|
+
executionInfo.status = 'completed';
|
|
907
|
+
executionInfo.exit_code = code || 0;
|
|
908
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
909
|
+
// 実行時間は全体(フォアグラウンド + バックグラウンド)で計算
|
|
910
|
+
if (executionInfo.started_at) {
|
|
911
|
+
const startTime = new Date(executionInfo.started_at).getTime();
|
|
912
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
913
|
+
}
|
|
914
|
+
this.executions.set(executionId, executionInfo);
|
|
915
|
+
// adaptive modeバックグラウンドプロセス正常終了のコールバック呼び出し
|
|
916
|
+
if (this.backgroundProcessCallbacks.onComplete) {
|
|
917
|
+
setImmediate(async () => {
|
|
918
|
+
try {
|
|
919
|
+
const callback = this.backgroundProcessCallbacks.onComplete;
|
|
920
|
+
if (callback) {
|
|
921
|
+
const result = callback(executionId, executionInfo);
|
|
922
|
+
if (result instanceof Promise) {
|
|
923
|
+
await result;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
catch (callbackError) {
|
|
928
|
+
// コールバックエラーは内部ログに記録のみ
|
|
929
|
+
// console.error('Adaptive background process completion callback error:', callbackError);
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
childProcess.on('error', (error) => {
|
|
936
|
+
clearTimeout(timeout);
|
|
937
|
+
if (childProcess.pid) {
|
|
938
|
+
this.processes.delete(childProcess.pid);
|
|
939
|
+
}
|
|
940
|
+
const executionInfo = this.executions.get(executionId);
|
|
941
|
+
if (executionInfo) {
|
|
942
|
+
executionInfo.status = 'failed';
|
|
943
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
944
|
+
if (executionInfo.started_at) {
|
|
945
|
+
const startTime = new Date(executionInfo.started_at).getTime();
|
|
946
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
947
|
+
}
|
|
948
|
+
this.executions.set(executionId, executionInfo);
|
|
949
|
+
// adaptive modeバックグラウンドプロセスエラーのコールバック呼び出し
|
|
950
|
+
if (this.backgroundProcessCallbacks.onError) {
|
|
951
|
+
setImmediate(async () => {
|
|
952
|
+
try {
|
|
953
|
+
const callback = this.backgroundProcessCallbacks.onError;
|
|
954
|
+
if (callback) {
|
|
955
|
+
const result = callback(executionId, executionInfo, error);
|
|
956
|
+
if (result instanceof Promise) {
|
|
957
|
+
await result;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
catch (callbackError) {
|
|
962
|
+
// コールバックエラーは内部ログに記録のみ
|
|
963
|
+
// console.error('Adaptive background process error callback error:', callbackError);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
async executeDetachedCommand(executionId, options) {
|
|
971
|
+
// detachedモード: 完全にバックグラウンドで実行し、親プロセスとの接続を切断
|
|
972
|
+
const env = getSafeEnvironment(process.env, options.environmentVariables);
|
|
973
|
+
const childProcess = spawn('/bin/bash', ['-c', options.command], {
|
|
974
|
+
cwd: this.resolveWorkingDirectory(options.workingDirectory),
|
|
975
|
+
env,
|
|
976
|
+
stdio: ['ignore', 'pipe', 'pipe'], // stdin は無視
|
|
977
|
+
detached: true, // 完全にデタッチ
|
|
978
|
+
});
|
|
979
|
+
// デタッチされたプロセスのPIDは記録するが、プロセス管理からは除外
|
|
980
|
+
const executionInfo = this.executions.get(executionId);
|
|
981
|
+
if (executionInfo && childProcess.pid !== undefined) {
|
|
982
|
+
executionInfo.process_id = childProcess.pid;
|
|
983
|
+
executionInfo.status = 'running';
|
|
984
|
+
this.executions.set(executionId, executionInfo);
|
|
985
|
+
}
|
|
986
|
+
// デタッチされたプロセスは親プロセスの終了後も継続実行されるため、
|
|
987
|
+
// 出力の収集は限定的
|
|
988
|
+
const startTime = Date.now();
|
|
989
|
+
let stdout = '';
|
|
990
|
+
let stderr = '';
|
|
991
|
+
if (childProcess.stdout) {
|
|
992
|
+
childProcess.stdout.on('data', (data) => {
|
|
993
|
+
stdout += data.toString();
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
if (childProcess.stderr) {
|
|
997
|
+
childProcess.stderr.on('data', (data) => {
|
|
998
|
+
stderr += data.toString();
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
// プロセスの終了を監視(デタッチされているため必ずしも捕捉されない)
|
|
1002
|
+
childProcess.on('close', async (code) => {
|
|
1003
|
+
const executionInfo = this.executions.get(executionId);
|
|
1004
|
+
if (executionInfo) {
|
|
1005
|
+
executionInfo.status = 'completed';
|
|
1006
|
+
executionInfo.exit_code = code || 0;
|
|
1007
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
1008
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
1009
|
+
try {
|
|
1010
|
+
const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
|
|
1011
|
+
executionInfo.output_id = outputFileId;
|
|
1012
|
+
}
|
|
1013
|
+
catch (error) {
|
|
1014
|
+
// ファイル保存失敗は重要なエラーとしてログに記録し、実行情報に含める
|
|
1015
|
+
console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
|
|
1016
|
+
executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1017
|
+
}
|
|
1018
|
+
this.executions.set(executionId, executionInfo);
|
|
1019
|
+
// detachedプロセス正常終了のコールバック呼び出し
|
|
1020
|
+
if (this.backgroundProcessCallbacks.onComplete) {
|
|
1021
|
+
setImmediate(async () => {
|
|
1022
|
+
try {
|
|
1023
|
+
const callback = this.backgroundProcessCallbacks.onComplete;
|
|
1024
|
+
if (callback) {
|
|
1025
|
+
const result = callback(executionId, executionInfo);
|
|
1026
|
+
if (result instanceof Promise) {
|
|
1027
|
+
await result;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
catch (callbackError) {
|
|
1032
|
+
// コールバックエラーは内部ログに記録のみ
|
|
1033
|
+
// console.error('Detached process completion callback error:', callbackError);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
childProcess.on('error', (error) => {
|
|
1040
|
+
const executionInfo = this.executions.get(executionId);
|
|
1041
|
+
if (executionInfo) {
|
|
1042
|
+
executionInfo.status = 'failed';
|
|
1043
|
+
executionInfo.execution_time_ms = Date.now() - startTime;
|
|
1044
|
+
executionInfo.completed_at = getCurrentTimestamp();
|
|
1045
|
+
this.executions.set(executionId, executionInfo);
|
|
1046
|
+
// detachedプロセスエラーのコールバック呼び出し
|
|
1047
|
+
if (this.backgroundProcessCallbacks.onError) {
|
|
1048
|
+
setImmediate(async () => {
|
|
1049
|
+
try {
|
|
1050
|
+
const callback = this.backgroundProcessCallbacks.onError;
|
|
1051
|
+
if (callback) {
|
|
1052
|
+
const result = callback(executionId, executionInfo, error);
|
|
1053
|
+
if (result instanceof Promise) {
|
|
1054
|
+
await result;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
catch (callbackError) {
|
|
1059
|
+
// コールバックエラーは内部ログに記録のみ
|
|
1060
|
+
// console.error('Detached process error callback error:', callbackError);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
// プロセスをデタッチ
|
|
1067
|
+
childProcess.unref();
|
|
1068
|
+
const resultExecutionInfo = this.executions.get(executionId);
|
|
1069
|
+
if (!resultExecutionInfo) {
|
|
1070
|
+
throw new Error(`Execution info not found for ID: ${executionId}`);
|
|
1071
|
+
}
|
|
1072
|
+
return resultExecutionInfo;
|
|
1073
|
+
}
|
|
1074
|
+
async saveOutputToFile(executionId, stdout, stderr) {
|
|
1075
|
+
if (!this.fileManager) {
|
|
1076
|
+
// FileManagerが利用できない場合は、従来の方法でファイルを保存
|
|
1077
|
+
const outputFileId = generateId();
|
|
1078
|
+
const filePath = path.join(this.outputDir, `${outputFileId}.json`);
|
|
1079
|
+
const outputData = {
|
|
1080
|
+
execution_id: executionId,
|
|
1081
|
+
stdout,
|
|
1082
|
+
stderr,
|
|
1083
|
+
created_at: getCurrentTimestamp(),
|
|
1084
|
+
};
|
|
1085
|
+
await fs.writeFile(filePath, JSON.stringify(outputData, null, 2), 'utf-8');
|
|
1086
|
+
return outputFileId;
|
|
1087
|
+
}
|
|
1088
|
+
// FileManagerを使用して出力ファイルを作成
|
|
1089
|
+
const combinedOutput = stdout + (stderr ? '\n--- STDERR ---\n' + stderr : '');
|
|
1090
|
+
return await this.fileManager.createOutputFile(combinedOutput, executionId);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* 出力状態の詳細情報を設定するヘルパー関数
|
|
1094
|
+
* Issue #14: Enhanced guidance messages for adaptive mode transitions
|
|
1095
|
+
* 改善: outputTruncated の代わりに reason ベースで状態を判定
|
|
1096
|
+
*/
|
|
1097
|
+
setOutputStatus(executionInfo, actuallyTruncated, // 実際に出力が切り捨てられたか
|
|
1098
|
+
reason, outputId) {
|
|
1099
|
+
// reasonに基づいて出力状態を設定
|
|
1100
|
+
const needsGuidance = !!outputId; // output_idがあれば常にガイダンスを提供
|
|
1101
|
+
// 後方互換性のため outputTruncated を設定
|
|
1102
|
+
executionInfo.output_truncated =
|
|
1103
|
+
actuallyTruncated || reason === 'timeout' || reason === 'background_transition';
|
|
1104
|
+
// Issue #14: バックグラウンド移行とタイムアウトは特別扱い
|
|
1105
|
+
if (reason === 'background_transition') {
|
|
1106
|
+
executionInfo.truncation_reason = reason;
|
|
1107
|
+
executionInfo.output_status = {
|
|
1108
|
+
complete: false, // バックグラウンド実行中は未完了
|
|
1109
|
+
reason: reason,
|
|
1110
|
+
available_via_output_id: !!outputId,
|
|
1111
|
+
recommended_action: outputId ? 'use_read_execution_output' : undefined,
|
|
1112
|
+
};
|
|
1113
|
+
executionInfo.message =
|
|
1114
|
+
'Command moved to background execution. Use process_list to monitor progress.';
|
|
1115
|
+
executionInfo.next_steps = [
|
|
1116
|
+
'Use process_list to check status',
|
|
1117
|
+
'Use read_execution_output when completed',
|
|
1118
|
+
'Use output_id for real-time pipeline processing',
|
|
1119
|
+
];
|
|
1120
|
+
if (needsGuidance) {
|
|
1121
|
+
executionInfo.guidance = {
|
|
1122
|
+
pipeline_usage: `Background process active. Use "input_output_id": "${outputId}" for real-time processing`,
|
|
1123
|
+
suggested_commands: [
|
|
1124
|
+
'tail -f equivalent using input_output_id for live monitoring',
|
|
1125
|
+
'grep for real-time log filtering',
|
|
1126
|
+
'awk for live data extraction and formatting',
|
|
1127
|
+
],
|
|
1128
|
+
background_processing: {
|
|
1129
|
+
status_check: 'Use process_get_execution for detailed status',
|
|
1130
|
+
monitoring: 'Output_id supports real-time streaming while process runs',
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (reason === 'timeout') {
|
|
1137
|
+
executionInfo.truncation_reason = reason;
|
|
1138
|
+
executionInfo.output_status = {
|
|
1139
|
+
complete: false, // タイムアウトは未完了
|
|
1140
|
+
reason: reason,
|
|
1141
|
+
available_via_output_id: !!outputId,
|
|
1142
|
+
recommended_action: outputId ? 'use_read_execution_output' : undefined,
|
|
1143
|
+
};
|
|
1144
|
+
executionInfo.message = `Command timed out. ${outputId ? 'Use read_execution_output with output_id for complete results.' : 'Partial output available.'}`;
|
|
1145
|
+
if (needsGuidance) {
|
|
1146
|
+
executionInfo.next_steps = [
|
|
1147
|
+
'Use read_execution_output to get complete output',
|
|
1148
|
+
'Use output_id for pipeline processing with grep/sed/awk commands',
|
|
1149
|
+
];
|
|
1150
|
+
executionInfo.guidance = {
|
|
1151
|
+
pipeline_usage: `Use "input_output_id": "${outputId}" parameter for further processing`,
|
|
1152
|
+
suggested_commands: [
|
|
1153
|
+
'grep pattern search using input_output_id',
|
|
1154
|
+
'sed text transformations using input_output_id',
|
|
1155
|
+
'awk data processing using input_output_id',
|
|
1156
|
+
],
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
// 実際に出力が切り捨てられた場合
|
|
1162
|
+
if (actuallyTruncated) {
|
|
1163
|
+
executionInfo.truncation_reason = reason;
|
|
1164
|
+
executionInfo.output_status = {
|
|
1165
|
+
complete: false,
|
|
1166
|
+
reason: reason,
|
|
1167
|
+
available_via_output_id: !!outputId,
|
|
1168
|
+
recommended_action: outputId ? 'use_read_execution_output' : undefined,
|
|
1169
|
+
};
|
|
1170
|
+
// 状況に応じたメッセージとアクションの設定
|
|
1171
|
+
switch (reason) {
|
|
1172
|
+
case 'size_limit':
|
|
1173
|
+
executionInfo.message = `Output exceeded size limit. ${outputId ? 'Complete output available via output_id.' : 'Output was truncated.'}`;
|
|
1174
|
+
if (needsGuidance) {
|
|
1175
|
+
executionInfo.next_steps = [
|
|
1176
|
+
'Use read_execution_output to get complete output',
|
|
1177
|
+
'Use output_id for streaming pipeline processing',
|
|
1178
|
+
];
|
|
1179
|
+
executionInfo.guidance = {
|
|
1180
|
+
pipeline_usage: `Large output detected. Use "input_output_id": "${outputId}" for efficient processing`,
|
|
1181
|
+
suggested_commands: [
|
|
1182
|
+
'head/tail for output sampling using input_output_id',
|
|
1183
|
+
'grep for pattern matching without loading full output',
|
|
1184
|
+
'wc for counting lines/words/bytes efficiently',
|
|
1185
|
+
],
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
break;
|
|
1189
|
+
default:
|
|
1190
|
+
executionInfo.message = `Output truncated due to ${reason}. ${outputId ? 'Complete output may be available via output_id.' : ''}`;
|
|
1191
|
+
if (needsGuidance) {
|
|
1192
|
+
executionInfo.next_steps = [
|
|
1193
|
+
'Use read_execution_output to get complete output',
|
|
1194
|
+
'Use output_id for pipeline processing',
|
|
1195
|
+
];
|
|
1196
|
+
executionInfo.guidance = {
|
|
1197
|
+
pipeline_usage: `Use "input_output_id": "${outputId}" parameter for further processing`,
|
|
1198
|
+
suggested_commands: [
|
|
1199
|
+
'grep for pattern searching',
|
|
1200
|
+
'sed for text transformations',
|
|
1201
|
+
'awk for data processing',
|
|
1202
|
+
],
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
// 完了した場合(切り捨てなし)
|
|
1209
|
+
executionInfo.output_status = {
|
|
1210
|
+
complete: true,
|
|
1211
|
+
available_via_output_id: !!outputId,
|
|
1212
|
+
};
|
|
1213
|
+
// Issue #14: Add guidance even for complete outputs to promote pipeline usage
|
|
1214
|
+
if (needsGuidance) {
|
|
1215
|
+
executionInfo.guidance = {
|
|
1216
|
+
pipeline_usage: `Output saved. Use "input_output_id": "${outputId}" for further processing`,
|
|
1217
|
+
suggested_commands: [
|
|
1218
|
+
'grep for pattern searching',
|
|
1219
|
+
'sed for text transformations',
|
|
1220
|
+
'awk for data processing and formatting',
|
|
1221
|
+
],
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
getExecution(executionId) {
|
|
1227
|
+
return this.executions.get(executionId);
|
|
1228
|
+
}
|
|
1229
|
+
listExecutions(filter) {
|
|
1230
|
+
let executions = Array.from(this.executions.values());
|
|
1231
|
+
// フィルタリング
|
|
1232
|
+
if (filter) {
|
|
1233
|
+
if (filter.status) {
|
|
1234
|
+
executions = executions.filter((exec) => exec.status === filter.status);
|
|
1235
|
+
}
|
|
1236
|
+
if (filter.commandPattern) {
|
|
1237
|
+
const pattern = new RegExp(filter.commandPattern, 'i');
|
|
1238
|
+
executions = executions.filter((exec) => pattern.test(exec.command));
|
|
1239
|
+
}
|
|
1240
|
+
if (filter.sessionId) {
|
|
1241
|
+
// セッション管理は今後実装
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const total = executions.length;
|
|
1245
|
+
// ページネーション
|
|
1246
|
+
if (filter?.offset || filter?.limit) {
|
|
1247
|
+
const offset = filter.offset || 0;
|
|
1248
|
+
const limit = filter.limit || 50;
|
|
1249
|
+
executions = executions.slice(offset, offset + limit);
|
|
1250
|
+
}
|
|
1251
|
+
return { executions, total };
|
|
1252
|
+
}
|
|
1253
|
+
async killProcess(processId, signal = 'TERM', force = false) {
|
|
1254
|
+
const childProcess = this.processes.get(processId);
|
|
1255
|
+
if (!childProcess) {
|
|
1256
|
+
throw new ResourceNotFoundError('process', processId.toString());
|
|
1257
|
+
}
|
|
1258
|
+
try {
|
|
1259
|
+
// プロセスを終了
|
|
1260
|
+
const signalName = signal === 'KILL' ? 'SIGKILL' : `SIG${signal}`;
|
|
1261
|
+
const killed = childProcess.kill(signalName);
|
|
1262
|
+
if (!killed && force && signal !== 'KILL') {
|
|
1263
|
+
// 強制終了
|
|
1264
|
+
childProcess.kill('SIGKILL');
|
|
1265
|
+
}
|
|
1266
|
+
// プロセスが終了するまで待機
|
|
1267
|
+
await new Promise((resolve) => {
|
|
1268
|
+
childProcess.on('close', () => resolve());
|
|
1269
|
+
setTimeout(() => resolve(), 5000); // 5秒でタイムアウト
|
|
1270
|
+
});
|
|
1271
|
+
this.processes.delete(processId);
|
|
1272
|
+
return {
|
|
1273
|
+
success: true,
|
|
1274
|
+
signal_sent: signal,
|
|
1275
|
+
exit_code: childProcess.exitCode || undefined,
|
|
1276
|
+
message: 'Process terminated successfully',
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
catch (error) {
|
|
1280
|
+
return {
|
|
1281
|
+
success: false,
|
|
1282
|
+
signal_sent: signal,
|
|
1283
|
+
message: `Failed to kill process: ${error}`,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
listProcesses() {
|
|
1288
|
+
const processes = [];
|
|
1289
|
+
for (const [pid] of this.processes) {
|
|
1290
|
+
// 対応する実行情報を検索
|
|
1291
|
+
const execution = Array.from(this.executions.values()).find((exec) => exec.process_id === pid);
|
|
1292
|
+
if (execution) {
|
|
1293
|
+
const processInfo = {
|
|
1294
|
+
process_id: pid,
|
|
1295
|
+
execution_id: execution.execution_id,
|
|
1296
|
+
command: execution.command,
|
|
1297
|
+
status: execution.status,
|
|
1298
|
+
created_at: execution.created_at,
|
|
1299
|
+
};
|
|
1300
|
+
if (execution.working_directory) {
|
|
1301
|
+
processInfo.working_directory = execution.working_directory;
|
|
1302
|
+
}
|
|
1303
|
+
if (execution.environment_variables) {
|
|
1304
|
+
processInfo.environment_variables = execution.environment_variables;
|
|
1305
|
+
}
|
|
1306
|
+
if (execution.started_at) {
|
|
1307
|
+
processInfo.started_at = execution.started_at;
|
|
1308
|
+
}
|
|
1309
|
+
if (execution.completed_at) {
|
|
1310
|
+
processInfo.completed_at = execution.completed_at;
|
|
1311
|
+
}
|
|
1312
|
+
processes.push(processInfo);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return processes;
|
|
1316
|
+
}
|
|
1317
|
+
cleanup() {
|
|
1318
|
+
// 実行中のプロセスを全て終了
|
|
1319
|
+
for (const [, childProcess] of this.processes) {
|
|
1320
|
+
try {
|
|
1321
|
+
childProcess.kill('SIGTERM');
|
|
1322
|
+
setTimeout(() => {
|
|
1323
|
+
if (!childProcess.killed) {
|
|
1324
|
+
childProcess.kill('SIGKILL');
|
|
1325
|
+
}
|
|
1326
|
+
}, 5000);
|
|
1327
|
+
}
|
|
1328
|
+
catch (error) {
|
|
1329
|
+
// エラーログを内部ログに記録(標準出力を避ける)
|
|
1330
|
+
// console.error(`Failed to cleanup process ${pid}:`, error);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
this.processes.clear();
|
|
1334
|
+
this.executions.clear();
|
|
1335
|
+
}
|
|
1336
|
+
// ワーキングディレクトリ管理
|
|
1337
|
+
setDefaultWorkingDirectory(workingDirectory) {
|
|
1338
|
+
const previousWorkdir = this.defaultWorkingDirectory;
|
|
1339
|
+
// ディレクトリの検証
|
|
1340
|
+
if (!this.isAllowedWorkingDirectory(workingDirectory)) {
|
|
1341
|
+
throw new Error(`Working directory not allowed: ${workingDirectory}`);
|
|
1342
|
+
}
|
|
1343
|
+
this.defaultWorkingDirectory = workingDirectory;
|
|
1344
|
+
return {
|
|
1345
|
+
success: true,
|
|
1346
|
+
previous_working_directory: previousWorkdir,
|
|
1347
|
+
new_working_directory: workingDirectory,
|
|
1348
|
+
working_directory_changed: previousWorkdir !== workingDirectory,
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
getDefaultWorkingDirectory() {
|
|
1352
|
+
return this.defaultWorkingDirectory;
|
|
1353
|
+
}
|
|
1354
|
+
getAllowedWorkingDirectories() {
|
|
1355
|
+
return [...this.allowedWorkingDirectories];
|
|
1356
|
+
}
|
|
1357
|
+
isAllowedWorkingDirectory(workingDirectory) {
|
|
1358
|
+
// パスの正規化を行って比較
|
|
1359
|
+
const normalizedPath = path.resolve(workingDirectory);
|
|
1360
|
+
return this.allowedWorkingDirectories.some((allowedDir) => {
|
|
1361
|
+
const normalizedAllowed = path.resolve(allowedDir);
|
|
1362
|
+
return (normalizedPath === normalizedAllowed ||
|
|
1363
|
+
normalizedPath.startsWith(normalizedAllowed + path.sep));
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
resolveWorkingDirectory(workingDirectory) {
|
|
1367
|
+
const resolved = workingDirectory || this.defaultWorkingDirectory;
|
|
1368
|
+
if (!this.isAllowedWorkingDirectory(resolved)) {
|
|
1369
|
+
throw new Error(`Working directory not allowed: ${resolved}`);
|
|
1370
|
+
}
|
|
1371
|
+
return resolved;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
//# sourceMappingURL=process-manager.js.map
|