scripts-orchestrator 2.10.0 → 2.13.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/index.js +48 -1
- package/lib/health-check.js +27 -11
- package/lib/logger.js +142 -12
- package/lib/orchestrator.js +387 -72
- package/lib/process-manager.js +336 -100
- package/package.json +1 -1
- package/scripts-orchestrator.config.js +6 -0
package/lib/process-manager.js
CHANGED
|
@@ -4,7 +4,6 @@ import path from 'path';
|
|
|
4
4
|
import { log } from './logger.js';
|
|
5
5
|
import { HealthCheck } from './health-check.js';
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
export class ProcessManager {
|
|
9
8
|
constructor() {
|
|
10
9
|
this.logger = log;
|
|
@@ -13,20 +12,39 @@ export class ProcessManager {
|
|
|
13
12
|
this.logFolder = 'scripts-orchestrator-logs'; // Default log folder
|
|
14
13
|
}
|
|
15
14
|
|
|
15
|
+
formatDuration(ms) {
|
|
16
|
+
if (ms < 1000) return `${ms}ms`;
|
|
17
|
+
const seconds = Math.floor(ms / 1000);
|
|
18
|
+
const minutes = Math.floor(seconds / 60);
|
|
19
|
+
const remainingSeconds = seconds % 60;
|
|
20
|
+
if (minutes > 0) {
|
|
21
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
22
|
+
}
|
|
23
|
+
return `${seconds}s`;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
setLogFolder(logFolder) {
|
|
17
27
|
this.logFolder = logFolder;
|
|
18
28
|
this.logger.verbose(`Log folder set to: ${logFolder}`);
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
getLogPath(command) {
|
|
22
|
-
const baseDir = this.logFolder
|
|
32
|
+
const baseDir = this.logFolder
|
|
33
|
+
? path.resolve(this.logFolder)
|
|
34
|
+
: process.cwd();
|
|
23
35
|
const LOGS_DIR = path.join(baseDir, 'scripts-orchestrator-logs');
|
|
24
36
|
// Use only the first word of the command for the log filename
|
|
25
37
|
const logName = command.split(/\s+/)[0];
|
|
26
38
|
return path.join(LOGS_DIR, `${logName}.log`);
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
addBackgroundProcess({
|
|
41
|
+
addBackgroundProcess({
|
|
42
|
+
command,
|
|
43
|
+
url,
|
|
44
|
+
startedByScript,
|
|
45
|
+
process_tracking,
|
|
46
|
+
kill_command,
|
|
47
|
+
}) {
|
|
30
48
|
this.logger.verbose(`Adding background process: ${command} (${url})`);
|
|
31
49
|
this.backgroundProcessesDetails.push({
|
|
32
50
|
command,
|
|
@@ -37,8 +55,46 @@ export class ProcessManager {
|
|
|
37
55
|
});
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
parseGnuTimeOutput(filePath) {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(filePath)) return null;
|
|
61
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
62
|
+
const m = text.match(/Maximum resident set size \(kbytes\):\s*(\d+)/i);
|
|
63
|
+
const kbytes = m ? parseInt(m[1], 10) : null;
|
|
64
|
+
try {
|
|
65
|
+
fs.unlinkSync(filePath);
|
|
66
|
+
} catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
return Number.isFinite(kbytes) ? kbytes : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Parse macOS BSD time -l output (bytes) from text; returns memory in KB or null. */
|
|
76
|
+
parseBsdTimeOutput(text) {
|
|
77
|
+
if (!text || typeof text !== 'string') return null;
|
|
78
|
+
const m = text.match(/(\d+)\s+maximum resident set size/i);
|
|
79
|
+
if (!m) return null;
|
|
80
|
+
const bytes = parseInt(m[1], 10);
|
|
81
|
+
return Number.isFinite(bytes) ? Math.round(bytes / 1024) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async runCommand({
|
|
85
|
+
cmd,
|
|
86
|
+
logFile,
|
|
87
|
+
background = false,
|
|
88
|
+
healthCheck = null,
|
|
89
|
+
kill_command = null,
|
|
90
|
+
isRetry = false,
|
|
91
|
+
env = null,
|
|
92
|
+
reportTime = false,
|
|
93
|
+
reportMemory = false,
|
|
94
|
+
}) {
|
|
95
|
+
const baseDir = this.logFolder
|
|
96
|
+
? path.resolve(this.logFolder)
|
|
97
|
+
: process.cwd();
|
|
42
98
|
const LOGS_DIR = path.join(baseDir, 'scripts-orchestrator-logs');
|
|
43
99
|
// Use only the first word of the command for the log filename
|
|
44
100
|
const logName = cmd.split(/\s+/)[0];
|
|
@@ -54,26 +110,45 @@ export class ProcessManager {
|
|
|
54
110
|
this.logger.verbose(`Clearing log file at ${LOG_FILE}`);
|
|
55
111
|
fs.writeFileSync(LOG_FILE, ''); // Clear the log file
|
|
56
112
|
} else {
|
|
57
|
-
this.logger.verbose(
|
|
113
|
+
this.logger.verbose(
|
|
114
|
+
`Appending to existing log file at ${LOG_FILE} (retry attempt)`,
|
|
115
|
+
);
|
|
58
116
|
}
|
|
59
117
|
} catch (error) {
|
|
60
118
|
this.logger.error(`Failed to setup log file: ${error.message}`);
|
|
61
|
-
return Promise.resolve({
|
|
119
|
+
return Promise.resolve({
|
|
120
|
+
success: false,
|
|
121
|
+
output: '',
|
|
122
|
+
durationMs: 0,
|
|
123
|
+
memoryKb: null,
|
|
124
|
+
});
|
|
62
125
|
}
|
|
63
126
|
|
|
64
127
|
return new Promise((resolve) => {
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
let timeOutputPath = null;
|
|
65
130
|
// Build command with environment variables if provided
|
|
66
131
|
let fullCommand = `npm run ${cmd}`;
|
|
67
132
|
if (env && Object.keys(env).length > 0) {
|
|
68
|
-
const envStr = Object.entries(env)
|
|
133
|
+
const envStr = Object.entries(env)
|
|
134
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
135
|
+
.join(' ');
|
|
69
136
|
fullCommand = `${envStr} npm run ${cmd}`;
|
|
70
137
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
138
|
+
const useTimeWrapper =
|
|
139
|
+
reportMemory && !background && (process.platform === 'linux' || process.platform === 'darwin');
|
|
140
|
+
if (useTimeWrapper && process.platform === 'linux') {
|
|
141
|
+
timeOutputPath = path.join(LOGS_DIR, `.time-${logName}-${startTime}.txt`);
|
|
142
|
+
fullCommand = `/usr/bin/time -v -o ${JSON.stringify(timeOutputPath)} sh -c ${JSON.stringify(fullCommand)}`;
|
|
143
|
+
} else if (useTimeWrapper && process.platform === 'darwin') {
|
|
144
|
+
fullCommand = `/usr/bin/time -l sh -c ${JSON.stringify(fullCommand)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.logger.startTask(cmd, fullCommand);
|
|
148
|
+
|
|
74
149
|
// Create isolated environment for each process
|
|
75
150
|
const isolatedEnv = this.createIsolatedEnvironment({ command: cmd, env });
|
|
76
|
-
|
|
151
|
+
|
|
77
152
|
const options = {
|
|
78
153
|
shell: true,
|
|
79
154
|
detached: background,
|
|
@@ -91,30 +166,41 @@ export class ProcessManager {
|
|
|
91
166
|
const processInstance = spawn(fullCommand, [], options);
|
|
92
167
|
|
|
93
168
|
processInstance.on('error', (error) => {
|
|
169
|
+
this.logger.stopTask(cmd);
|
|
94
170
|
this.logger.error(`Failed to start process: ${error.message}`);
|
|
95
|
-
|
|
96
|
-
|
|
171
|
+
resolve({
|
|
172
|
+
success: false,
|
|
173
|
+
output: '',
|
|
174
|
+
durationMs: Date.now() - startTime,
|
|
175
|
+
memoryKb: null,
|
|
176
|
+
});
|
|
97
177
|
});
|
|
98
178
|
|
|
99
179
|
if (background) {
|
|
100
180
|
const processGroupId = processInstance.pid;
|
|
101
|
-
this.logger.verbose(
|
|
181
|
+
this.logger.verbose(
|
|
182
|
+
`Background process spawned with PID: ${processGroupId}`,
|
|
183
|
+
);
|
|
102
184
|
|
|
103
185
|
// Track process exit for background processes
|
|
104
186
|
let processExited = false;
|
|
105
187
|
let processExitCode = null;
|
|
106
|
-
|
|
188
|
+
|
|
107
189
|
processInstance.on('exit', (code, signal) => {
|
|
108
190
|
processExited = true;
|
|
109
191
|
processExitCode = code;
|
|
110
|
-
this.logger.verbose(
|
|
192
|
+
this.logger.verbose(
|
|
193
|
+
`Background process ${cmd} (PID: ${processGroupId}) exited with code: ${code}, signal: ${signal}`,
|
|
194
|
+
);
|
|
111
195
|
});
|
|
112
196
|
|
|
113
197
|
processInstance.stdout.on('data', (data) => {
|
|
114
198
|
try {
|
|
115
199
|
fs.appendFileSync(LOG_FILE, data.toString());
|
|
116
200
|
} catch (error) {
|
|
117
|
-
this.logger.error(
|
|
201
|
+
this.logger.error(
|
|
202
|
+
`Failed to write to log file: ${error.message}`,
|
|
203
|
+
);
|
|
118
204
|
}
|
|
119
205
|
});
|
|
120
206
|
|
|
@@ -122,7 +208,9 @@ export class ProcessManager {
|
|
|
122
208
|
try {
|
|
123
209
|
fs.appendFileSync(LOG_FILE, data.toString());
|
|
124
210
|
} catch (error) {
|
|
125
|
-
this.logger.error(
|
|
211
|
+
this.logger.error(
|
|
212
|
+
`Failed to write to log file: ${error.message}`,
|
|
213
|
+
);
|
|
126
214
|
}
|
|
127
215
|
});
|
|
128
216
|
|
|
@@ -134,37 +222,59 @@ export class ProcessManager {
|
|
|
134
222
|
try {
|
|
135
223
|
// First check if the process has already exited with an error
|
|
136
224
|
if (processExited && processExitCode !== 0) {
|
|
137
|
-
this.logger.
|
|
225
|
+
this.logger.stopTask(cmd);
|
|
226
|
+
this.logger.error(
|
|
227
|
+
`Background process ${cmd} exited with code ${processExitCode}`,
|
|
228
|
+
);
|
|
138
229
|
let output = '';
|
|
139
230
|
try {
|
|
140
231
|
output = fs.readFileSync(LOG_FILE, 'utf8');
|
|
141
232
|
this.logger.verbose(`Process output: ${output}`);
|
|
142
233
|
} catch (error) {
|
|
143
|
-
this.logger.error(
|
|
234
|
+
this.logger.error(
|
|
235
|
+
`Failed to read log file: ${error.message}`,
|
|
236
|
+
);
|
|
144
237
|
}
|
|
145
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
success: false,
|
|
240
|
+
output,
|
|
241
|
+
durationMs: Date.now() - startTime,
|
|
242
|
+
memoryKb: null,
|
|
243
|
+
};
|
|
146
244
|
}
|
|
147
|
-
|
|
148
|
-
this.logger.verbose(
|
|
245
|
+
|
|
246
|
+
this.logger.verbose(
|
|
247
|
+
`Verifying process ${processGroupId} (attempt ${attempt}/${maxAttempts})`,
|
|
248
|
+
);
|
|
149
249
|
process.kill(processGroupId, 0);
|
|
150
250
|
this.logger.verbose(`Process ${processGroupId} is running`);
|
|
151
|
-
|
|
251
|
+
|
|
152
252
|
// Wait a bit more to ensure the process doesn't exit immediately after verification
|
|
153
253
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
154
|
-
|
|
254
|
+
|
|
155
255
|
// Check again if the process exited during our wait
|
|
156
256
|
if (processExited && processExitCode !== 0) {
|
|
157
|
-
this.logger.
|
|
257
|
+
this.logger.stopTask(cmd);
|
|
258
|
+
this.logger.error(
|
|
259
|
+
`Background process ${cmd} exited with code ${processExitCode} shortly after starting`,
|
|
260
|
+
);
|
|
158
261
|
let output = '';
|
|
159
262
|
try {
|
|
160
263
|
output = fs.readFileSync(LOG_FILE, 'utf8');
|
|
161
264
|
this.logger.verbose(`Process output: ${output}`);
|
|
162
265
|
} catch (error) {
|
|
163
|
-
this.logger.error(
|
|
266
|
+
this.logger.error(
|
|
267
|
+
`Failed to read log file: ${error.message}`,
|
|
268
|
+
);
|
|
164
269
|
}
|
|
165
|
-
return {
|
|
270
|
+
return {
|
|
271
|
+
success: false,
|
|
272
|
+
output,
|
|
273
|
+
durationMs: Date.now() - startTime,
|
|
274
|
+
memoryKb: null,
|
|
275
|
+
};
|
|
166
276
|
}
|
|
167
|
-
|
|
277
|
+
|
|
168
278
|
this.backgroundProcesses.push(processGroupId);
|
|
169
279
|
this.backgroundProcessesDetails.push({
|
|
170
280
|
command: cmd,
|
|
@@ -174,28 +284,52 @@ export class ProcessManager {
|
|
|
174
284
|
startedByScript: true,
|
|
175
285
|
kill_command,
|
|
176
286
|
});
|
|
177
|
-
|
|
287
|
+
|
|
178
288
|
this.logger.verbose(`Unreferencing process ${processGroupId}`);
|
|
179
289
|
processInstance.unref();
|
|
180
|
-
|
|
290
|
+
|
|
291
|
+
this.logger.stopTask(cmd);
|
|
181
292
|
this.logger.verbose(
|
|
182
293
|
`Background process started: npm run ${cmd} (PGID: ${processGroupId})`,
|
|
183
294
|
);
|
|
184
|
-
return {
|
|
295
|
+
return {
|
|
296
|
+
success: true,
|
|
297
|
+
output: '',
|
|
298
|
+
durationMs: Date.now() - startTime,
|
|
299
|
+
memoryKb: null,
|
|
300
|
+
};
|
|
185
301
|
} catch (error) {
|
|
186
302
|
if (attempt === maxAttempts) {
|
|
187
|
-
this.logger.error(
|
|
188
|
-
|
|
189
|
-
|
|
303
|
+
this.logger.error(
|
|
304
|
+
`Failed to start background process: npm run ${cmd}`,
|
|
305
|
+
);
|
|
306
|
+
this.logger.verbose(
|
|
307
|
+
`Final verification attempt failed: ${error.message}`,
|
|
308
|
+
);
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
output: '',
|
|
312
|
+
durationMs: Date.now() - startTime,
|
|
313
|
+
memoryKb: null,
|
|
314
|
+
};
|
|
190
315
|
}
|
|
191
|
-
this.logger.verbose(
|
|
192
|
-
|
|
316
|
+
this.logger.verbose(
|
|
317
|
+
`Verification attempt ${attempt} failed: ${error.message}`,
|
|
318
|
+
);
|
|
319
|
+
this.logger.verbose(
|
|
320
|
+
`Waiting ${baseDelay * Math.pow(2, attempt - 1)}ms before next attempt`,
|
|
321
|
+
);
|
|
193
322
|
await new Promise((resolve) =>
|
|
194
323
|
setTimeout(resolve, baseDelay * Math.pow(2, attempt - 1)),
|
|
195
324
|
);
|
|
196
325
|
}
|
|
197
326
|
}
|
|
198
|
-
return {
|
|
327
|
+
return {
|
|
328
|
+
success: false,
|
|
329
|
+
output: '',
|
|
330
|
+
durationMs: Date.now() - startTime,
|
|
331
|
+
memoryKb: null,
|
|
332
|
+
};
|
|
199
333
|
};
|
|
200
334
|
|
|
201
335
|
verifyProcess().then(resolve);
|
|
@@ -204,7 +338,9 @@ export class ProcessManager {
|
|
|
204
338
|
try {
|
|
205
339
|
fs.appendFileSync(LOG_FILE, data.toString());
|
|
206
340
|
} catch (error) {
|
|
207
|
-
this.logger.error(
|
|
341
|
+
this.logger.error(
|
|
342
|
+
`Failed to write to log file: ${error.message}`,
|
|
343
|
+
);
|
|
208
344
|
}
|
|
209
345
|
});
|
|
210
346
|
|
|
@@ -212,7 +348,9 @@ export class ProcessManager {
|
|
|
212
348
|
try {
|
|
213
349
|
fs.appendFileSync(LOG_FILE, data.toString());
|
|
214
350
|
} catch (error) {
|
|
215
|
-
this.logger.error(
|
|
351
|
+
this.logger.error(
|
|
352
|
+
`Failed to write to log file: ${error.message}`,
|
|
353
|
+
);
|
|
216
354
|
}
|
|
217
355
|
});
|
|
218
356
|
|
|
@@ -224,20 +362,52 @@ export class ProcessManager {
|
|
|
224
362
|
this.logger.error(`Failed to read log file: ${error.message}`);
|
|
225
363
|
}
|
|
226
364
|
|
|
365
|
+
this.logger.stopTask(cmd);
|
|
366
|
+
|
|
367
|
+
const durationMs = Date.now() - startTime;
|
|
368
|
+
const durationStr = reportTime
|
|
369
|
+
? ` (${this.formatDuration(durationMs)})`
|
|
370
|
+
: '';
|
|
371
|
+
let memoryKb = null;
|
|
372
|
+
if (reportMemory) {
|
|
373
|
+
if (timeOutputPath) {
|
|
374
|
+
memoryKb = this.parseGnuTimeOutput(timeOutputPath);
|
|
375
|
+
} else if (process.platform === 'darwin') {
|
|
376
|
+
memoryKb = this.parseBsdTimeOutput(output);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
227
380
|
if (code !== 0) {
|
|
228
|
-
this.logger.error(
|
|
381
|
+
this.logger.error(
|
|
382
|
+
`Failed: npm run ${cmd} ❌${durationStr} (exit code: ${code})`,
|
|
383
|
+
);
|
|
229
384
|
this.logger.verbose(`Process output: ${output}`);
|
|
230
|
-
resolve({
|
|
385
|
+
resolve({
|
|
386
|
+
success: false,
|
|
387
|
+
output,
|
|
388
|
+
durationMs,
|
|
389
|
+
memoryKb,
|
|
390
|
+
});
|
|
231
391
|
} else {
|
|
232
|
-
this.logger.success(`Completed: npm run ${cmd}`);
|
|
233
|
-
resolve({
|
|
392
|
+
this.logger.success(`Completed: npm run ${cmd} ✅${durationStr}`);
|
|
393
|
+
resolve({
|
|
394
|
+
success: true,
|
|
395
|
+
output,
|
|
396
|
+
durationMs,
|
|
397
|
+
memoryKb,
|
|
398
|
+
});
|
|
234
399
|
}
|
|
235
400
|
});
|
|
236
401
|
}
|
|
237
402
|
} catch (error) {
|
|
403
|
+
this.logger.stopTask(cmd);
|
|
238
404
|
this.logger.error(`Failed to spawn process: ${error.message}`);
|
|
239
|
-
|
|
240
|
-
|
|
405
|
+
resolve({
|
|
406
|
+
success: false,
|
|
407
|
+
output: '',
|
|
408
|
+
durationMs: Date.now() - startTime,
|
|
409
|
+
memoryKb: null,
|
|
410
|
+
});
|
|
241
411
|
}
|
|
242
412
|
});
|
|
243
413
|
}
|
|
@@ -245,7 +415,7 @@ export class ProcessManager {
|
|
|
245
415
|
createIsolatedEnvironment({ command, env = null }) {
|
|
246
416
|
// Create a deep copy to avoid any reference sharing
|
|
247
417
|
const baseEnv = JSON.parse(JSON.stringify(process.env));
|
|
248
|
-
|
|
418
|
+
|
|
249
419
|
// Set standard environment variables
|
|
250
420
|
const isolatedEnv = {
|
|
251
421
|
...baseEnv,
|
|
@@ -272,25 +442,37 @@ export class ProcessManager {
|
|
|
272
442
|
// Remove any potentially problematic environment variables
|
|
273
443
|
delete isolatedEnv.npm_lifecycle_event;
|
|
274
444
|
delete isolatedEnv.npm_lifecycle_script;
|
|
275
|
-
|
|
445
|
+
|
|
276
446
|
return isolatedEnv;
|
|
277
447
|
}
|
|
278
448
|
|
|
279
449
|
async cleanup() {
|
|
280
450
|
try {
|
|
281
451
|
this.logger.info('\nCleaning up background processes...');
|
|
282
|
-
|
|
452
|
+
|
|
283
453
|
// Debug: Log the number of processes we're tracking
|
|
284
|
-
this.logger.info(
|
|
285
|
-
|
|
454
|
+
this.logger.info(
|
|
455
|
+
`- Found ${this.backgroundProcessesDetails.length} background processes to clean up`,
|
|
456
|
+
);
|
|
457
|
+
|
|
286
458
|
// Debug: Log each process details
|
|
287
|
-
this.backgroundProcessesDetails.forEach(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
459
|
+
this.backgroundProcessesDetails.forEach(
|
|
460
|
+
({ command, pgid, url, startedByScript, kill_command }, index) => {
|
|
461
|
+
this.logger.verbose(
|
|
462
|
+
`- Process ${index + 1}: command=${command}, pgid=${pgid}, url=${url}, startedByScript=${startedByScript}, kill_command=${kill_command}`,
|
|
463
|
+
);
|
|
464
|
+
},
|
|
465
|
+
);
|
|
466
|
+
|
|
291
467
|
const killPromises = this.backgroundProcessesDetails.map(
|
|
292
468
|
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
293
|
-
await this.cleanupProcess({
|
|
469
|
+
await this.cleanupProcess({
|
|
470
|
+
command,
|
|
471
|
+
pgid,
|
|
472
|
+
url,
|
|
473
|
+
startedByScript,
|
|
474
|
+
kill_command,
|
|
475
|
+
});
|
|
294
476
|
},
|
|
295
477
|
);
|
|
296
478
|
|
|
@@ -304,33 +486,43 @@ export class ProcessManager {
|
|
|
304
486
|
|
|
305
487
|
async cleanupCommand(commandName) {
|
|
306
488
|
this.logger.info(`\nCleaning up processes for command: ${commandName}`);
|
|
307
|
-
|
|
489
|
+
|
|
308
490
|
// Find processes for this specific command
|
|
309
491
|
const commandProcesses = this.backgroundProcessesDetails.filter(
|
|
310
|
-
({ command }) => command === commandName
|
|
492
|
+
({ command }) => command === commandName,
|
|
311
493
|
);
|
|
312
|
-
|
|
494
|
+
|
|
313
495
|
if (commandProcesses.length === 0) {
|
|
314
|
-
this.logger.verbose(
|
|
496
|
+
this.logger.verbose(
|
|
497
|
+
`- No background processes found for command: ${commandName}`,
|
|
498
|
+
);
|
|
315
499
|
return;
|
|
316
500
|
}
|
|
317
|
-
|
|
318
|
-
this.logger.verbose(
|
|
319
|
-
|
|
501
|
+
|
|
502
|
+
this.logger.verbose(
|
|
503
|
+
`- Found ${commandProcesses.length} background processes for command: ${commandName}`,
|
|
504
|
+
);
|
|
505
|
+
|
|
320
506
|
const killPromises = commandProcesses.map(
|
|
321
507
|
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
322
|
-
await this.cleanupProcess({
|
|
323
|
-
|
|
508
|
+
await this.cleanupProcess({
|
|
509
|
+
command,
|
|
510
|
+
pgid,
|
|
511
|
+
url,
|
|
512
|
+
startedByScript,
|
|
513
|
+
kill_command,
|
|
514
|
+
});
|
|
515
|
+
},
|
|
324
516
|
);
|
|
325
517
|
|
|
326
518
|
await Promise.allSettled(killPromises);
|
|
327
|
-
|
|
519
|
+
|
|
328
520
|
// Remove the cleaned up processes from our tracking arrays
|
|
329
|
-
this.backgroundProcesses = this.backgroundProcesses.filter(
|
|
330
|
-
!commandProcesses.some(proc => proc.pgid === pgid)
|
|
521
|
+
this.backgroundProcesses = this.backgroundProcesses.filter(
|
|
522
|
+
(pgid) => !commandProcesses.some((proc) => proc.pgid === pgid),
|
|
331
523
|
);
|
|
332
524
|
this.backgroundProcessesDetails = this.backgroundProcessesDetails.filter(
|
|
333
|
-
({ command }) => command !== commandName
|
|
525
|
+
({ command }) => command !== commandName,
|
|
334
526
|
);
|
|
335
527
|
}
|
|
336
528
|
|
|
@@ -342,24 +534,40 @@ export class ProcessManager {
|
|
|
342
534
|
return;
|
|
343
535
|
}
|
|
344
536
|
|
|
345
|
-
this.logger.verbose(
|
|
537
|
+
this.logger.verbose(
|
|
538
|
+
`- Processing cleanup for ${command} (kill_command: ${kill_command})`,
|
|
539
|
+
);
|
|
346
540
|
|
|
347
541
|
// Try custom kill command first if specified
|
|
348
542
|
if (kill_command) {
|
|
349
543
|
try {
|
|
350
|
-
this.logger.verbose(
|
|
351
|
-
|
|
544
|
+
this.logger.verbose(
|
|
545
|
+
`- Using custom kill command: npm run ${kill_command}`,
|
|
546
|
+
);
|
|
547
|
+
const result = await this.runCommand({
|
|
548
|
+
cmd: kill_command,
|
|
549
|
+
logFile: null,
|
|
550
|
+
background: false,
|
|
551
|
+
});
|
|
352
552
|
if (result.success) {
|
|
353
|
-
this.logger.verbose(
|
|
553
|
+
this.logger.verbose(
|
|
554
|
+
`- Successfully killed ${command} using custom command`,
|
|
555
|
+
);
|
|
354
556
|
return;
|
|
355
557
|
} else {
|
|
356
|
-
this.logger.verbose(
|
|
558
|
+
this.logger.verbose(
|
|
559
|
+
'- Custom kill command failed, falling back to process signals',
|
|
560
|
+
);
|
|
357
561
|
}
|
|
358
562
|
} catch (error) {
|
|
359
|
-
this.logger.verbose(
|
|
563
|
+
this.logger.verbose(
|
|
564
|
+
`- Custom kill command error: ${error.message}, falling back`,
|
|
565
|
+
);
|
|
360
566
|
}
|
|
361
567
|
} else {
|
|
362
|
-
this.logger.verbose(
|
|
568
|
+
this.logger.verbose(
|
|
569
|
+
`- No kill_command specified for ${command}, using process signals`,
|
|
570
|
+
);
|
|
363
571
|
}
|
|
364
572
|
|
|
365
573
|
try {
|
|
@@ -375,27 +583,36 @@ export class ProcessManager {
|
|
|
375
583
|
|
|
376
584
|
// Cross-platform process termination
|
|
377
585
|
const isWindows = process.platform === 'win32';
|
|
378
|
-
|
|
586
|
+
|
|
379
587
|
if (isWindows) {
|
|
380
588
|
// Windows: use taskkill to terminate process tree
|
|
381
589
|
try {
|
|
382
|
-
const killProcess = spawn('taskkill', [
|
|
590
|
+
const killProcess = spawn('taskkill', [
|
|
591
|
+
'/F',
|
|
592
|
+
'/T',
|
|
593
|
+
'/PID',
|
|
594
|
+
pgid.toString(),
|
|
595
|
+
]);
|
|
383
596
|
await new Promise((resolve) => {
|
|
384
597
|
killProcess.on('close', resolve);
|
|
385
598
|
});
|
|
386
|
-
this.logger.verbose(
|
|
599
|
+
this.logger.verbose(
|
|
600
|
+
`- Terminated background process: ${command} (PID: ${pgid})`,
|
|
601
|
+
);
|
|
387
602
|
return;
|
|
388
603
|
} catch (killError) {
|
|
389
|
-
this.logger.verbose(
|
|
604
|
+
this.logger.verbose(
|
|
605
|
+
`- Failed to use taskkill, falling back to process.kill: ${killError.message}`,
|
|
606
|
+
);
|
|
390
607
|
}
|
|
391
608
|
}
|
|
392
|
-
|
|
609
|
+
|
|
393
610
|
// Unix/Linux/macOS or Windows fallback: Try SIGTERM first
|
|
394
611
|
process.kill(pgid, 'SIGTERM');
|
|
395
612
|
|
|
396
613
|
await new Promise((resolve, reject) => {
|
|
397
614
|
let timeout, checkInterval;
|
|
398
|
-
|
|
615
|
+
|
|
399
616
|
timeout = setTimeout(() => {
|
|
400
617
|
if (checkInterval) clearInterval(checkInterval);
|
|
401
618
|
reject(new Error('Process termination timeout'));
|
|
@@ -415,26 +632,31 @@ export class ProcessManager {
|
|
|
415
632
|
`- Terminated background process: ${command} (PGID: ${pgid})`,
|
|
416
633
|
);
|
|
417
634
|
} catch (error) {
|
|
418
|
-
this.logger.verbose(
|
|
635
|
+
this.logger.verbose(
|
|
636
|
+
`- Failed to terminate process group: ${error.message}`,
|
|
637
|
+
);
|
|
419
638
|
}
|
|
420
639
|
|
|
421
640
|
// Check if the URL is still responding after termination attempt
|
|
422
641
|
if (url) {
|
|
423
642
|
try {
|
|
424
643
|
const urlObj = new URL(url);
|
|
425
|
-
const port =
|
|
426
|
-
|
|
644
|
+
const port =
|
|
645
|
+
urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80');
|
|
646
|
+
|
|
427
647
|
// Use shared HTTP utility for cross-platform compatibility
|
|
428
648
|
const urlResult = await HealthCheck.makeHttpRequest(url, 2000);
|
|
429
|
-
|
|
649
|
+
|
|
430
650
|
if (urlResult.success && urlResult.statusCode === 200) {
|
|
431
|
-
this.logger.verbose(
|
|
432
|
-
|
|
651
|
+
this.logger.verbose(
|
|
652
|
+
`- URL ${url} is still responding after termination, finding process on port ${port}`,
|
|
653
|
+
);
|
|
654
|
+
|
|
433
655
|
// Find and kill process using the port - cross-platform approach
|
|
434
656
|
try {
|
|
435
657
|
const isWindows = process.platform === 'win32';
|
|
436
658
|
let findPortCmd, findPortArgs;
|
|
437
|
-
|
|
659
|
+
|
|
438
660
|
if (isWindows) {
|
|
439
661
|
// Windows: use netstat
|
|
440
662
|
findPortCmd = 'netstat';
|
|
@@ -444,7 +666,7 @@ export class ProcessManager {
|
|
|
444
666
|
findPortCmd = 'lsof';
|
|
445
667
|
findPortArgs = ['-i', `:${port}`, '-t'];
|
|
446
668
|
}
|
|
447
|
-
|
|
669
|
+
|
|
448
670
|
const findProcess = spawn(findPortCmd, findPortArgs);
|
|
449
671
|
const result = await new Promise((resolve) => {
|
|
450
672
|
let output = '';
|
|
@@ -458,12 +680,15 @@ export class ProcessManager {
|
|
|
458
680
|
|
|
459
681
|
if (result.code === 0 && result.output.trim()) {
|
|
460
682
|
let pids = [];
|
|
461
|
-
|
|
683
|
+
|
|
462
684
|
if (isWindows) {
|
|
463
685
|
// Parse netstat output to find PIDs for the specific port
|
|
464
686
|
const lines = result.output.split('\n');
|
|
465
687
|
for (const line of lines) {
|
|
466
|
-
if (
|
|
688
|
+
if (
|
|
689
|
+
line.includes(`:${port} `) &&
|
|
690
|
+
line.includes('LISTENING')
|
|
691
|
+
) {
|
|
467
692
|
const parts = line.trim().split(/\s+/);
|
|
468
693
|
const pid = parts[parts.length - 1];
|
|
469
694
|
if (pid && !isNaN(pid)) {
|
|
@@ -475,7 +700,7 @@ export class ProcessManager {
|
|
|
475
700
|
// lsof output is already just PIDs
|
|
476
701
|
pids = result.output.trim().split('\n');
|
|
477
702
|
}
|
|
478
|
-
|
|
703
|
+
|
|
479
704
|
for (const pid of pids) {
|
|
480
705
|
try {
|
|
481
706
|
if (isWindows) {
|
|
@@ -488,16 +713,22 @@ export class ProcessManager {
|
|
|
488
713
|
// Unix/Linux/macOS: use process.kill
|
|
489
714
|
process.kill(parseInt(pid), 'SIGKILL');
|
|
490
715
|
}
|
|
491
|
-
this.logger.verbose(
|
|
716
|
+
this.logger.verbose(
|
|
717
|
+
`- Killed process (PID: ${pid}) using port ${port}`,
|
|
718
|
+
);
|
|
492
719
|
} catch (killError) {
|
|
493
720
|
if (killError.code !== 'ESRCH') {
|
|
494
|
-
this.logger.error(
|
|
721
|
+
this.logger.error(
|
|
722
|
+
`- Failed to kill process (PID: ${pid}): ${killError.message}`,
|
|
723
|
+
);
|
|
495
724
|
}
|
|
496
725
|
}
|
|
497
726
|
}
|
|
498
727
|
}
|
|
499
728
|
} catch (portError) {
|
|
500
|
-
this.logger.error(
|
|
729
|
+
this.logger.error(
|
|
730
|
+
`- Failed to find process using port ${port}: ${portError.message}`,
|
|
731
|
+
);
|
|
501
732
|
}
|
|
502
733
|
}
|
|
503
734
|
} catch (error) {
|
|
@@ -508,10 +739,15 @@ export class ProcessManager {
|
|
|
508
739
|
// Final attempt to kill the process group
|
|
509
740
|
try {
|
|
510
741
|
const isWindows = process.platform === 'win32';
|
|
511
|
-
|
|
742
|
+
|
|
512
743
|
if (isWindows) {
|
|
513
744
|
// Windows: force kill with taskkill
|
|
514
|
-
const killProcess = spawn('taskkill', [
|
|
745
|
+
const killProcess = spawn('taskkill', [
|
|
746
|
+
'/F',
|
|
747
|
+
'/T',
|
|
748
|
+
'/PID',
|
|
749
|
+
pgid.toString(),
|
|
750
|
+
]);
|
|
515
751
|
await new Promise((resolve) => {
|
|
516
752
|
killProcess.on('close', resolve);
|
|
517
753
|
});
|
|
@@ -528,4 +764,4 @@ export class ProcessManager {
|
|
|
528
764
|
}
|
|
529
765
|
|
|
530
766
|
// For backward compatibility
|
|
531
|
-
export const processManager = new ProcessManager();
|
|
767
|
+
export const processManager = new ProcessManager();
|