aiexecode 1.0.126 → 1.0.128

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.

Potentially problematic release.


This version of aiexecode might be problematic. Click here for more details.

@@ -0,0 +1,317 @@
1
+ /**
2
+ * 백그라운드 프로세스 관리 모듈
3
+ *
4
+ * AI Agent가 백그라운드로 명령어를 실행하고 관리할 수 있도록 지원
5
+ */
6
+
7
+ import { spawn } from 'child_process';
8
+ import { EventEmitter } from 'events';
9
+ import { createDebugLogger } from '../util/debug_log.js';
10
+ import { whichCommand } from './code_executer.js';
11
+
12
+ const debugLog = createDebugLogger('background_process.log', 'background_process');
13
+
14
+ /**
15
+ * 백그라운드 프로세스 정보
16
+ * @typedef {Object} BackgroundProcess
17
+ * @property {string} id - 고유 ID
18
+ * @property {string} command - 실행된 명령어
19
+ * @property {number} pid - 프로세스 ID
20
+ * @property {string} status - 상태 (running, completed, failed, killed)
21
+ * @property {Date} startedAt - 시작 시간
22
+ * @property {Date|null} endedAt - 종료 시간
23
+ * @property {string} stdout - 표준 출력
24
+ * @property {string} stderr - 표준 에러
25
+ * @property {number|null} exitCode - 종료 코드
26
+ */
27
+
28
+ class BackgroundProcessManager extends EventEmitter {
29
+ constructor() {
30
+ super();
31
+ /** @type {Map<string, BackgroundProcess>} */
32
+ this.processes = new Map();
33
+ this.nextId = 1;
34
+ }
35
+
36
+ /**
37
+ * 고유 ID 생성
38
+ */
39
+ _generateId() {
40
+ return `bg_${this.nextId++}`;
41
+ }
42
+
43
+ /**
44
+ * 백그라운드로 명령어 실행
45
+ * @param {string} script - 실행할 스크립트
46
+ * @param {Object} options - 옵션
47
+ * @returns {Promise<{id: string, pid: number}>}
48
+ */
49
+ async run(script, options = {}) {
50
+ const shellPath = await whichCommand("bash") || await whichCommand("sh");
51
+ if (!shellPath) {
52
+ throw new Error('No shell found');
53
+ }
54
+
55
+ const id = this._generateId();
56
+ const startedAt = new Date();
57
+
58
+ debugLog(`[BackgroundProcess] Starting: id=${id}, script="${script.substring(0, 50)}..."`);
59
+
60
+ const child = spawn(shellPath, ['-c', script], {
61
+ stdio: ['ignore', 'pipe', 'pipe'],
62
+ env: {
63
+ ...process.env,
64
+ DEBIAN_FRONTEND: 'noninteractive',
65
+ CI: 'true',
66
+ BATCH: '1',
67
+ },
68
+ cwd: options.cwd || process.cwd(),
69
+ detached: true,
70
+ });
71
+
72
+ const processInfo = {
73
+ id,
74
+ command: script,
75
+ pid: child.pid,
76
+ status: 'running',
77
+ startedAt,
78
+ endedAt: null,
79
+ stdout: '',
80
+ stderr: '',
81
+ exitCode: null,
82
+ _process: child, // 내부용
83
+ };
84
+
85
+ this.processes.set(id, processInfo);
86
+
87
+ // stdout 수집
88
+ child.stdout.on('data', (data) => {
89
+ processInfo.stdout += data.toString();
90
+ this.emit('output', { id, type: 'stdout', data: data.toString() });
91
+ });
92
+
93
+ // stderr 수집
94
+ child.stderr.on('data', (data) => {
95
+ processInfo.stderr += data.toString();
96
+ this.emit('output', { id, type: 'stderr', data: data.toString() });
97
+ });
98
+
99
+ // 프로세스 종료 처리
100
+ child.on('close', (code) => {
101
+ processInfo.status = code === 0 ? 'completed' : 'failed';
102
+ processInfo.exitCode = code;
103
+ processInfo.endedAt = new Date();
104
+ delete processInfo._process;
105
+
106
+ debugLog(`[BackgroundProcess] Closed: id=${id}, code=${code}`);
107
+ this.emit('close', { id, code, status: processInfo.status });
108
+ });
109
+
110
+ child.on('error', (err) => {
111
+ processInfo.status = 'failed';
112
+ processInfo.stderr += `\nError: ${err.message}`;
113
+ processInfo.endedAt = new Date();
114
+ delete processInfo._process;
115
+
116
+ debugLog(`[BackgroundProcess] Error: id=${id}, error=${err.message}`);
117
+ this.emit('error', { id, error: err });
118
+ });
119
+
120
+ this.emit('started', { id, pid: child.pid, command: script });
121
+
122
+ return { id, pid: child.pid };
123
+ }
124
+
125
+ /**
126
+ * 프로세스 종료
127
+ * @param {string} id - 프로세스 ID
128
+ * @param {string} signal - 시그널 (기본: SIGTERM)
129
+ * @returns {boolean} 성공 여부
130
+ */
131
+ kill(id, signal = 'SIGTERM') {
132
+ const processInfo = this.processes.get(id);
133
+ if (!processInfo) {
134
+ debugLog(`[BackgroundProcess] Kill failed: id=${id} not found`);
135
+ return false;
136
+ }
137
+
138
+ if (processInfo.status !== 'running') {
139
+ debugLog(`[BackgroundProcess] Kill skipped: id=${id} already ${processInfo.status}`);
140
+ return false;
141
+ }
142
+
143
+ const child = processInfo._process;
144
+ if (!child) {
145
+ return false;
146
+ }
147
+
148
+ try {
149
+ // 프로세스 그룹 전체 종료
150
+ process.kill(-child.pid, signal);
151
+ processInfo.status = 'killed';
152
+ processInfo.endedAt = new Date();
153
+ debugLog(`[BackgroundProcess] Killed: id=${id}, signal=${signal}`);
154
+ this.emit('killed', { id, signal });
155
+ return true;
156
+ } catch (err) {
157
+ try {
158
+ child.kill(signal);
159
+ processInfo.status = 'killed';
160
+ processInfo.endedAt = new Date();
161
+ debugLog(`[BackgroundProcess] Killed (fallback): id=${id}`);
162
+ this.emit('killed', { id, signal });
163
+ return true;
164
+ } catch (err2) {
165
+ debugLog(`[BackgroundProcess] Kill error: id=${id}, error=${err2.message}`);
166
+ return false;
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * 프로세스 정보 조회
173
+ * @param {string} id - 프로세스 ID
174
+ * @returns {BackgroundProcess|null}
175
+ */
176
+ get(id) {
177
+ const info = this.processes.get(id);
178
+ if (!info) return null;
179
+
180
+ // _process 필드 제외하고 반환
181
+ const { _process, ...publicInfo } = info;
182
+ return publicInfo;
183
+ }
184
+
185
+ /**
186
+ * 모든 프로세스 목록 조회
187
+ * @param {Object} filter - 필터 옵션
188
+ * @returns {BackgroundProcess[]}
189
+ */
190
+ list(filter = {}) {
191
+ let result = [];
192
+
193
+ for (const [id, info] of this.processes) {
194
+ const { _process, ...publicInfo } = info;
195
+
196
+ // 필터 적용
197
+ if (filter.status && publicInfo.status !== filter.status) continue;
198
+ if (filter.running && publicInfo.status !== 'running') continue;
199
+
200
+ result.push(publicInfo);
201
+ }
202
+
203
+ // 시작 시간순 정렬
204
+ result.sort((a, b) => b.startedAt - a.startedAt);
205
+
206
+ return result;
207
+ }
208
+
209
+ /**
210
+ * 실행 중인 프로세스 수
211
+ */
212
+ get runningCount() {
213
+ let count = 0;
214
+ for (const info of this.processes.values()) {
215
+ if (info.status === 'running') count++;
216
+ }
217
+ return count;
218
+ }
219
+
220
+ /**
221
+ * 완료된 프로세스 정리
222
+ * @param {number} maxAge - 최대 보관 시간 (ms)
223
+ */
224
+ cleanup(maxAge = 3600000) {
225
+ const now = Date.now();
226
+ const toDelete = [];
227
+
228
+ for (const [id, info] of this.processes) {
229
+ if (info.status !== 'running' && info.endedAt) {
230
+ if (now - info.endedAt.getTime() > maxAge) {
231
+ toDelete.push(id);
232
+ }
233
+ }
234
+ }
235
+
236
+ for (const id of toDelete) {
237
+ this.processes.delete(id);
238
+ debugLog(`[BackgroundProcess] Cleaned up: id=${id}`);
239
+ }
240
+
241
+ return toDelete.length;
242
+ }
243
+
244
+ /**
245
+ * 모든 실행 중인 프로세스 종료
246
+ */
247
+ async killAll() {
248
+ const killed = [];
249
+ for (const [id, info] of this.processes) {
250
+ if (info.status === 'running') {
251
+ if (this.kill(id)) {
252
+ killed.push(id);
253
+ }
254
+ }
255
+ }
256
+ return killed;
257
+ }
258
+ }
259
+
260
+ // 싱글톤 인스턴스
261
+ let instance = null;
262
+
263
+ /**
264
+ * BackgroundProcessManager 싱글톤 인스턴스 가져오기
265
+ */
266
+ export function getBackgroundProcessManager() {
267
+ if (!instance) {
268
+ instance = new BackgroundProcessManager();
269
+ }
270
+ return instance;
271
+ }
272
+
273
+ /**
274
+ * 백그라운드로 명령어 실행
275
+ * @param {string} script - 실행할 스크립트
276
+ * @param {Object} options - 옵션
277
+ * @returns {Promise<{id: string, pid: number}>}
278
+ */
279
+ export async function runInBackground(script, options = {}) {
280
+ const manager = getBackgroundProcessManager();
281
+ return await manager.run(script, options);
282
+ }
283
+
284
+ /**
285
+ * 백그라운드 프로세스 종료
286
+ * @param {string} id - 프로세스 ID
287
+ */
288
+ export function killBackgroundProcess(id) {
289
+ const manager = getBackgroundProcessManager();
290
+ return manager.kill(id);
291
+ }
292
+
293
+ /**
294
+ * 백그라운드 프로세스 목록 조회
295
+ */
296
+ export function listBackgroundProcesses(filter = {}) {
297
+ const manager = getBackgroundProcessManager();
298
+ return manager.list(filter);
299
+ }
300
+
301
+ /**
302
+ * 백그라운드 프로세스 정보 조회
303
+ */
304
+ export function getBackgroundProcess(id) {
305
+ const manager = getBackgroundProcessManager();
306
+ return manager.get(id);
307
+ }
308
+
309
+ /**
310
+ * 모든 백그라운드 프로세스 종료 (앱 종료 시)
311
+ */
312
+ export async function killAllBackgroundProcesses() {
313
+ const manager = getBackgroundProcessManager();
314
+ return await manager.killAll();
315
+ }
316
+
317
+ export { BackgroundProcessManager };