scripts-orchestrator 2.9.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/index.js +6 -1
- package/lib/health-check.js +27 -11
- package/lib/logger.js +142 -12
- package/lib/orchestrator.js +145 -65
- package/lib/process-manager.js +229 -88
- package/package.json +1 -1
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,18 @@ export class ProcessManager {
|
|
|
37
55
|
});
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
async runCommand({
|
|
41
|
-
|
|
58
|
+
async runCommand({
|
|
59
|
+
cmd,
|
|
60
|
+
logFile,
|
|
61
|
+
background = false,
|
|
62
|
+
healthCheck = null,
|
|
63
|
+
kill_command = null,
|
|
64
|
+
isRetry = false,
|
|
65
|
+
env = null,
|
|
66
|
+
}) {
|
|
67
|
+
const baseDir = this.logFolder
|
|
68
|
+
? path.resolve(this.logFolder)
|
|
69
|
+
: process.cwd();
|
|
42
70
|
const LOGS_DIR = path.join(baseDir, 'scripts-orchestrator-logs');
|
|
43
71
|
// Use only the first word of the command for the log filename
|
|
44
72
|
const logName = cmd.split(/\s+/)[0];
|
|
@@ -54,7 +82,9 @@ export class ProcessManager {
|
|
|
54
82
|
this.logger.verbose(`Clearing log file at ${LOG_FILE}`);
|
|
55
83
|
fs.writeFileSync(LOG_FILE, ''); // Clear the log file
|
|
56
84
|
} else {
|
|
57
|
-
this.logger.verbose(
|
|
85
|
+
this.logger.verbose(
|
|
86
|
+
`Appending to existing log file at ${LOG_FILE} (retry attempt)`,
|
|
87
|
+
);
|
|
58
88
|
}
|
|
59
89
|
} catch (error) {
|
|
60
90
|
this.logger.error(`Failed to setup log file: ${error.message}`);
|
|
@@ -62,18 +92,21 @@ export class ProcessManager {
|
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
return new Promise((resolve) => {
|
|
95
|
+
const startTime = Date.now();
|
|
65
96
|
// Build command with environment variables if provided
|
|
66
97
|
let fullCommand = `npm run ${cmd}`;
|
|
67
98
|
if (env && Object.keys(env).length > 0) {
|
|
68
|
-
const envStr = Object.entries(env)
|
|
99
|
+
const envStr = Object.entries(env)
|
|
100
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
101
|
+
.join(' ');
|
|
69
102
|
fullCommand = `${envStr} npm run ${cmd}`;
|
|
70
103
|
}
|
|
71
|
-
|
|
72
|
-
this.logger.
|
|
73
|
-
|
|
104
|
+
|
|
105
|
+
this.logger.startTask(cmd, fullCommand);
|
|
106
|
+
|
|
74
107
|
// Create isolated environment for each process
|
|
75
108
|
const isolatedEnv = this.createIsolatedEnvironment({ command: cmd, env });
|
|
76
|
-
|
|
109
|
+
|
|
77
110
|
const options = {
|
|
78
111
|
shell: true,
|
|
79
112
|
detached: background,
|
|
@@ -91,6 +124,7 @@ export class ProcessManager {
|
|
|
91
124
|
const processInstance = spawn(fullCommand, [], options);
|
|
92
125
|
|
|
93
126
|
processInstance.on('error', (error) => {
|
|
127
|
+
this.logger.stopTask(cmd);
|
|
94
128
|
this.logger.error(`Failed to start process: ${error.message}`);
|
|
95
129
|
//this.logger.verbose(`Process error details: ${JSON.stringify(error, null, 2)}`);
|
|
96
130
|
resolve({ success: false, output: '' });
|
|
@@ -98,23 +132,29 @@ export class ProcessManager {
|
|
|
98
132
|
|
|
99
133
|
if (background) {
|
|
100
134
|
const processGroupId = processInstance.pid;
|
|
101
|
-
this.logger.verbose(
|
|
135
|
+
this.logger.verbose(
|
|
136
|
+
`Background process spawned with PID: ${processGroupId}`,
|
|
137
|
+
);
|
|
102
138
|
|
|
103
139
|
// Track process exit for background processes
|
|
104
140
|
let processExited = false;
|
|
105
141
|
let processExitCode = null;
|
|
106
|
-
|
|
142
|
+
|
|
107
143
|
processInstance.on('exit', (code, signal) => {
|
|
108
144
|
processExited = true;
|
|
109
145
|
processExitCode = code;
|
|
110
|
-
this.logger.verbose(
|
|
146
|
+
this.logger.verbose(
|
|
147
|
+
`Background process ${cmd} (PID: ${processGroupId}) exited with code: ${code}, signal: ${signal}`,
|
|
148
|
+
);
|
|
111
149
|
});
|
|
112
150
|
|
|
113
151
|
processInstance.stdout.on('data', (data) => {
|
|
114
152
|
try {
|
|
115
153
|
fs.appendFileSync(LOG_FILE, data.toString());
|
|
116
154
|
} catch (error) {
|
|
117
|
-
this.logger.error(
|
|
155
|
+
this.logger.error(
|
|
156
|
+
`Failed to write to log file: ${error.message}`,
|
|
157
|
+
);
|
|
118
158
|
}
|
|
119
159
|
});
|
|
120
160
|
|
|
@@ -122,7 +162,9 @@ export class ProcessManager {
|
|
|
122
162
|
try {
|
|
123
163
|
fs.appendFileSync(LOG_FILE, data.toString());
|
|
124
164
|
} catch (error) {
|
|
125
|
-
this.logger.error(
|
|
165
|
+
this.logger.error(
|
|
166
|
+
`Failed to write to log file: ${error.message}`,
|
|
167
|
+
);
|
|
126
168
|
}
|
|
127
169
|
});
|
|
128
170
|
|
|
@@ -134,37 +176,49 @@ export class ProcessManager {
|
|
|
134
176
|
try {
|
|
135
177
|
// First check if the process has already exited with an error
|
|
136
178
|
if (processExited && processExitCode !== 0) {
|
|
137
|
-
this.logger.
|
|
179
|
+
this.logger.stopTask(cmd);
|
|
180
|
+
this.logger.error(
|
|
181
|
+
`Background process ${cmd} exited with code ${processExitCode}`,
|
|
182
|
+
);
|
|
138
183
|
let output = '';
|
|
139
184
|
try {
|
|
140
185
|
output = fs.readFileSync(LOG_FILE, 'utf8');
|
|
141
186
|
this.logger.verbose(`Process output: ${output}`);
|
|
142
187
|
} catch (error) {
|
|
143
|
-
this.logger.error(
|
|
188
|
+
this.logger.error(
|
|
189
|
+
`Failed to read log file: ${error.message}`,
|
|
190
|
+
);
|
|
144
191
|
}
|
|
145
192
|
return { success: false, output };
|
|
146
193
|
}
|
|
147
|
-
|
|
148
|
-
this.logger.verbose(
|
|
194
|
+
|
|
195
|
+
this.logger.verbose(
|
|
196
|
+
`Verifying process ${processGroupId} (attempt ${attempt}/${maxAttempts})`,
|
|
197
|
+
);
|
|
149
198
|
process.kill(processGroupId, 0);
|
|
150
199
|
this.logger.verbose(`Process ${processGroupId} is running`);
|
|
151
|
-
|
|
200
|
+
|
|
152
201
|
// Wait a bit more to ensure the process doesn't exit immediately after verification
|
|
153
202
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
154
|
-
|
|
203
|
+
|
|
155
204
|
// Check again if the process exited during our wait
|
|
156
205
|
if (processExited && processExitCode !== 0) {
|
|
157
|
-
this.logger.
|
|
206
|
+
this.logger.stopTask(cmd);
|
|
207
|
+
this.logger.error(
|
|
208
|
+
`Background process ${cmd} exited with code ${processExitCode} shortly after starting`,
|
|
209
|
+
);
|
|
158
210
|
let output = '';
|
|
159
211
|
try {
|
|
160
212
|
output = fs.readFileSync(LOG_FILE, 'utf8');
|
|
161
213
|
this.logger.verbose(`Process output: ${output}`);
|
|
162
214
|
} catch (error) {
|
|
163
|
-
this.logger.error(
|
|
215
|
+
this.logger.error(
|
|
216
|
+
`Failed to read log file: ${error.message}`,
|
|
217
|
+
);
|
|
164
218
|
}
|
|
165
219
|
return { success: false, output };
|
|
166
220
|
}
|
|
167
|
-
|
|
221
|
+
|
|
168
222
|
this.backgroundProcesses.push(processGroupId);
|
|
169
223
|
this.backgroundProcessesDetails.push({
|
|
170
224
|
command: cmd,
|
|
@@ -174,22 +228,31 @@ export class ProcessManager {
|
|
|
174
228
|
startedByScript: true,
|
|
175
229
|
kill_command,
|
|
176
230
|
});
|
|
177
|
-
|
|
231
|
+
|
|
178
232
|
this.logger.verbose(`Unreferencing process ${processGroupId}`);
|
|
179
233
|
processInstance.unref();
|
|
180
|
-
|
|
234
|
+
|
|
235
|
+
this.logger.stopTask(cmd);
|
|
181
236
|
this.logger.verbose(
|
|
182
237
|
`Background process started: npm run ${cmd} (PGID: ${processGroupId})`,
|
|
183
238
|
);
|
|
184
239
|
return { success: true, output: '' };
|
|
185
240
|
} catch (error) {
|
|
186
241
|
if (attempt === maxAttempts) {
|
|
187
|
-
this.logger.error(
|
|
188
|
-
|
|
242
|
+
this.logger.error(
|
|
243
|
+
`Failed to start background process: npm run ${cmd}`,
|
|
244
|
+
);
|
|
245
|
+
this.logger.verbose(
|
|
246
|
+
`Final verification attempt failed: ${error.message}`,
|
|
247
|
+
);
|
|
189
248
|
return { success: false, output: '' };
|
|
190
249
|
}
|
|
191
|
-
this.logger.verbose(
|
|
192
|
-
|
|
250
|
+
this.logger.verbose(
|
|
251
|
+
`Verification attempt ${attempt} failed: ${error.message}`,
|
|
252
|
+
);
|
|
253
|
+
this.logger.verbose(
|
|
254
|
+
`Waiting ${baseDelay * Math.pow(2, attempt - 1)}ms before next attempt`,
|
|
255
|
+
);
|
|
193
256
|
await new Promise((resolve) =>
|
|
194
257
|
setTimeout(resolve, baseDelay * Math.pow(2, attempt - 1)),
|
|
195
258
|
);
|
|
@@ -204,7 +267,9 @@ export class ProcessManager {
|
|
|
204
267
|
try {
|
|
205
268
|
fs.appendFileSync(LOG_FILE, data.toString());
|
|
206
269
|
} catch (error) {
|
|
207
|
-
this.logger.error(
|
|
270
|
+
this.logger.error(
|
|
271
|
+
`Failed to write to log file: ${error.message}`,
|
|
272
|
+
);
|
|
208
273
|
}
|
|
209
274
|
});
|
|
210
275
|
|
|
@@ -212,7 +277,9 @@ export class ProcessManager {
|
|
|
212
277
|
try {
|
|
213
278
|
fs.appendFileSync(LOG_FILE, data.toString());
|
|
214
279
|
} catch (error) {
|
|
215
|
-
this.logger.error(
|
|
280
|
+
this.logger.error(
|
|
281
|
+
`Failed to write to log file: ${error.message}`,
|
|
282
|
+
);
|
|
216
283
|
}
|
|
217
284
|
});
|
|
218
285
|
|
|
@@ -224,17 +291,25 @@ export class ProcessManager {
|
|
|
224
291
|
this.logger.error(`Failed to read log file: ${error.message}`);
|
|
225
292
|
}
|
|
226
293
|
|
|
294
|
+
this.logger.stopTask(cmd);
|
|
295
|
+
|
|
296
|
+
const duration = Date.now() - startTime;
|
|
297
|
+
const durationStr = ` (${this.formatDuration(duration)})`;
|
|
298
|
+
|
|
227
299
|
if (code !== 0) {
|
|
228
|
-
this.logger.error(
|
|
300
|
+
this.logger.error(
|
|
301
|
+
`Failed: npm run ${cmd} ❌${durationStr} (exit code: ${code})`,
|
|
302
|
+
);
|
|
229
303
|
this.logger.verbose(`Process output: ${output}`);
|
|
230
304
|
resolve({ success: false, output });
|
|
231
305
|
} else {
|
|
232
|
-
this.logger.success(`Completed: npm run ${cmd}`);
|
|
306
|
+
this.logger.success(`Completed: npm run ${cmd} ✅${durationStr}`);
|
|
233
307
|
resolve({ success: true, output });
|
|
234
308
|
}
|
|
235
309
|
});
|
|
236
310
|
}
|
|
237
311
|
} catch (error) {
|
|
312
|
+
this.logger.stopTask(cmd);
|
|
238
313
|
this.logger.error(`Failed to spawn process: ${error.message}`);
|
|
239
314
|
//this.logger.verbose(`Spawn error details: ${JSON.stringify(error, null, 2)}`);
|
|
240
315
|
resolve({ success: false, output: '' });
|
|
@@ -245,7 +320,7 @@ export class ProcessManager {
|
|
|
245
320
|
createIsolatedEnvironment({ command, env = null }) {
|
|
246
321
|
// Create a deep copy to avoid any reference sharing
|
|
247
322
|
const baseEnv = JSON.parse(JSON.stringify(process.env));
|
|
248
|
-
|
|
323
|
+
|
|
249
324
|
// Set standard environment variables
|
|
250
325
|
const isolatedEnv = {
|
|
251
326
|
...baseEnv,
|
|
@@ -272,25 +347,37 @@ export class ProcessManager {
|
|
|
272
347
|
// Remove any potentially problematic environment variables
|
|
273
348
|
delete isolatedEnv.npm_lifecycle_event;
|
|
274
349
|
delete isolatedEnv.npm_lifecycle_script;
|
|
275
|
-
|
|
350
|
+
|
|
276
351
|
return isolatedEnv;
|
|
277
352
|
}
|
|
278
353
|
|
|
279
354
|
async cleanup() {
|
|
280
355
|
try {
|
|
281
356
|
this.logger.info('\nCleaning up background processes...');
|
|
282
|
-
|
|
357
|
+
|
|
283
358
|
// Debug: Log the number of processes we're tracking
|
|
284
|
-
this.logger.info(
|
|
285
|
-
|
|
359
|
+
this.logger.info(
|
|
360
|
+
`- Found ${this.backgroundProcessesDetails.length} background processes to clean up`,
|
|
361
|
+
);
|
|
362
|
+
|
|
286
363
|
// Debug: Log each process details
|
|
287
|
-
this.backgroundProcessesDetails.forEach(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
364
|
+
this.backgroundProcessesDetails.forEach(
|
|
365
|
+
({ command, pgid, url, startedByScript, kill_command }, index) => {
|
|
366
|
+
this.logger.verbose(
|
|
367
|
+
`- Process ${index + 1}: command=${command}, pgid=${pgid}, url=${url}, startedByScript=${startedByScript}, kill_command=${kill_command}`,
|
|
368
|
+
);
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
|
|
291
372
|
const killPromises = this.backgroundProcessesDetails.map(
|
|
292
373
|
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
293
|
-
await this.cleanupProcess({
|
|
374
|
+
await this.cleanupProcess({
|
|
375
|
+
command,
|
|
376
|
+
pgid,
|
|
377
|
+
url,
|
|
378
|
+
startedByScript,
|
|
379
|
+
kill_command,
|
|
380
|
+
});
|
|
294
381
|
},
|
|
295
382
|
);
|
|
296
383
|
|
|
@@ -304,33 +391,43 @@ export class ProcessManager {
|
|
|
304
391
|
|
|
305
392
|
async cleanupCommand(commandName) {
|
|
306
393
|
this.logger.info(`\nCleaning up processes for command: ${commandName}`);
|
|
307
|
-
|
|
394
|
+
|
|
308
395
|
// Find processes for this specific command
|
|
309
396
|
const commandProcesses = this.backgroundProcessesDetails.filter(
|
|
310
|
-
({ command }) => command === commandName
|
|
397
|
+
({ command }) => command === commandName,
|
|
311
398
|
);
|
|
312
|
-
|
|
399
|
+
|
|
313
400
|
if (commandProcesses.length === 0) {
|
|
314
|
-
this.logger.verbose(
|
|
401
|
+
this.logger.verbose(
|
|
402
|
+
`- No background processes found for command: ${commandName}`,
|
|
403
|
+
);
|
|
315
404
|
return;
|
|
316
405
|
}
|
|
317
|
-
|
|
318
|
-
this.logger.verbose(
|
|
319
|
-
|
|
406
|
+
|
|
407
|
+
this.logger.verbose(
|
|
408
|
+
`- Found ${commandProcesses.length} background processes for command: ${commandName}`,
|
|
409
|
+
);
|
|
410
|
+
|
|
320
411
|
const killPromises = commandProcesses.map(
|
|
321
412
|
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
322
|
-
await this.cleanupProcess({
|
|
323
|
-
|
|
413
|
+
await this.cleanupProcess({
|
|
414
|
+
command,
|
|
415
|
+
pgid,
|
|
416
|
+
url,
|
|
417
|
+
startedByScript,
|
|
418
|
+
kill_command,
|
|
419
|
+
});
|
|
420
|
+
},
|
|
324
421
|
);
|
|
325
422
|
|
|
326
423
|
await Promise.allSettled(killPromises);
|
|
327
|
-
|
|
424
|
+
|
|
328
425
|
// Remove the cleaned up processes from our tracking arrays
|
|
329
|
-
this.backgroundProcesses = this.backgroundProcesses.filter(
|
|
330
|
-
!commandProcesses.some(proc => proc.pgid === pgid)
|
|
426
|
+
this.backgroundProcesses = this.backgroundProcesses.filter(
|
|
427
|
+
(pgid) => !commandProcesses.some((proc) => proc.pgid === pgid),
|
|
331
428
|
);
|
|
332
429
|
this.backgroundProcessesDetails = this.backgroundProcessesDetails.filter(
|
|
333
|
-
({ command }) => command !== commandName
|
|
430
|
+
({ command }) => command !== commandName,
|
|
334
431
|
);
|
|
335
432
|
}
|
|
336
433
|
|
|
@@ -342,24 +439,40 @@ export class ProcessManager {
|
|
|
342
439
|
return;
|
|
343
440
|
}
|
|
344
441
|
|
|
345
|
-
this.logger.verbose(
|
|
442
|
+
this.logger.verbose(
|
|
443
|
+
`- Processing cleanup for ${command} (kill_command: ${kill_command})`,
|
|
444
|
+
);
|
|
346
445
|
|
|
347
446
|
// Try custom kill command first if specified
|
|
348
447
|
if (kill_command) {
|
|
349
448
|
try {
|
|
350
|
-
this.logger.verbose(
|
|
351
|
-
|
|
449
|
+
this.logger.verbose(
|
|
450
|
+
`- Using custom kill command: npm run ${kill_command}`,
|
|
451
|
+
);
|
|
452
|
+
const result = await this.runCommand({
|
|
453
|
+
cmd: kill_command,
|
|
454
|
+
logFile: null,
|
|
455
|
+
background: false,
|
|
456
|
+
});
|
|
352
457
|
if (result.success) {
|
|
353
|
-
this.logger.verbose(
|
|
458
|
+
this.logger.verbose(
|
|
459
|
+
`- Successfully killed ${command} using custom command`,
|
|
460
|
+
);
|
|
354
461
|
return;
|
|
355
462
|
} else {
|
|
356
|
-
this.logger.verbose(
|
|
463
|
+
this.logger.verbose(
|
|
464
|
+
'- Custom kill command failed, falling back to process signals',
|
|
465
|
+
);
|
|
357
466
|
}
|
|
358
467
|
} catch (error) {
|
|
359
|
-
this.logger.verbose(
|
|
468
|
+
this.logger.verbose(
|
|
469
|
+
`- Custom kill command error: ${error.message}, falling back`,
|
|
470
|
+
);
|
|
360
471
|
}
|
|
361
472
|
} else {
|
|
362
|
-
this.logger.verbose(
|
|
473
|
+
this.logger.verbose(
|
|
474
|
+
`- No kill_command specified for ${command}, using process signals`,
|
|
475
|
+
);
|
|
363
476
|
}
|
|
364
477
|
|
|
365
478
|
try {
|
|
@@ -375,27 +488,36 @@ export class ProcessManager {
|
|
|
375
488
|
|
|
376
489
|
// Cross-platform process termination
|
|
377
490
|
const isWindows = process.platform === 'win32';
|
|
378
|
-
|
|
491
|
+
|
|
379
492
|
if (isWindows) {
|
|
380
493
|
// Windows: use taskkill to terminate process tree
|
|
381
494
|
try {
|
|
382
|
-
const killProcess = spawn('taskkill', [
|
|
495
|
+
const killProcess = spawn('taskkill', [
|
|
496
|
+
'/F',
|
|
497
|
+
'/T',
|
|
498
|
+
'/PID',
|
|
499
|
+
pgid.toString(),
|
|
500
|
+
]);
|
|
383
501
|
await new Promise((resolve) => {
|
|
384
502
|
killProcess.on('close', resolve);
|
|
385
503
|
});
|
|
386
|
-
this.logger.verbose(
|
|
504
|
+
this.logger.verbose(
|
|
505
|
+
`- Terminated background process: ${command} (PID: ${pgid})`,
|
|
506
|
+
);
|
|
387
507
|
return;
|
|
388
508
|
} catch (killError) {
|
|
389
|
-
this.logger.verbose(
|
|
509
|
+
this.logger.verbose(
|
|
510
|
+
`- Failed to use taskkill, falling back to process.kill: ${killError.message}`,
|
|
511
|
+
);
|
|
390
512
|
}
|
|
391
513
|
}
|
|
392
|
-
|
|
514
|
+
|
|
393
515
|
// Unix/Linux/macOS or Windows fallback: Try SIGTERM first
|
|
394
516
|
process.kill(pgid, 'SIGTERM');
|
|
395
517
|
|
|
396
518
|
await new Promise((resolve, reject) => {
|
|
397
519
|
let timeout, checkInterval;
|
|
398
|
-
|
|
520
|
+
|
|
399
521
|
timeout = setTimeout(() => {
|
|
400
522
|
if (checkInterval) clearInterval(checkInterval);
|
|
401
523
|
reject(new Error('Process termination timeout'));
|
|
@@ -415,26 +537,31 @@ export class ProcessManager {
|
|
|
415
537
|
`- Terminated background process: ${command} (PGID: ${pgid})`,
|
|
416
538
|
);
|
|
417
539
|
} catch (error) {
|
|
418
|
-
this.logger.verbose(
|
|
540
|
+
this.logger.verbose(
|
|
541
|
+
`- Failed to terminate process group: ${error.message}`,
|
|
542
|
+
);
|
|
419
543
|
}
|
|
420
544
|
|
|
421
545
|
// Check if the URL is still responding after termination attempt
|
|
422
546
|
if (url) {
|
|
423
547
|
try {
|
|
424
548
|
const urlObj = new URL(url);
|
|
425
|
-
const port =
|
|
426
|
-
|
|
549
|
+
const port =
|
|
550
|
+
urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80');
|
|
551
|
+
|
|
427
552
|
// Use shared HTTP utility for cross-platform compatibility
|
|
428
553
|
const urlResult = await HealthCheck.makeHttpRequest(url, 2000);
|
|
429
|
-
|
|
554
|
+
|
|
430
555
|
if (urlResult.success && urlResult.statusCode === 200) {
|
|
431
|
-
this.logger.verbose(
|
|
432
|
-
|
|
556
|
+
this.logger.verbose(
|
|
557
|
+
`- URL ${url} is still responding after termination, finding process on port ${port}`,
|
|
558
|
+
);
|
|
559
|
+
|
|
433
560
|
// Find and kill process using the port - cross-platform approach
|
|
434
561
|
try {
|
|
435
562
|
const isWindows = process.platform === 'win32';
|
|
436
563
|
let findPortCmd, findPortArgs;
|
|
437
|
-
|
|
564
|
+
|
|
438
565
|
if (isWindows) {
|
|
439
566
|
// Windows: use netstat
|
|
440
567
|
findPortCmd = 'netstat';
|
|
@@ -444,7 +571,7 @@ export class ProcessManager {
|
|
|
444
571
|
findPortCmd = 'lsof';
|
|
445
572
|
findPortArgs = ['-i', `:${port}`, '-t'];
|
|
446
573
|
}
|
|
447
|
-
|
|
574
|
+
|
|
448
575
|
const findProcess = spawn(findPortCmd, findPortArgs);
|
|
449
576
|
const result = await new Promise((resolve) => {
|
|
450
577
|
let output = '';
|
|
@@ -458,12 +585,15 @@ export class ProcessManager {
|
|
|
458
585
|
|
|
459
586
|
if (result.code === 0 && result.output.trim()) {
|
|
460
587
|
let pids = [];
|
|
461
|
-
|
|
588
|
+
|
|
462
589
|
if (isWindows) {
|
|
463
590
|
// Parse netstat output to find PIDs for the specific port
|
|
464
591
|
const lines = result.output.split('\n');
|
|
465
592
|
for (const line of lines) {
|
|
466
|
-
if (
|
|
593
|
+
if (
|
|
594
|
+
line.includes(`:${port} `) &&
|
|
595
|
+
line.includes('LISTENING')
|
|
596
|
+
) {
|
|
467
597
|
const parts = line.trim().split(/\s+/);
|
|
468
598
|
const pid = parts[parts.length - 1];
|
|
469
599
|
if (pid && !isNaN(pid)) {
|
|
@@ -475,7 +605,7 @@ export class ProcessManager {
|
|
|
475
605
|
// lsof output is already just PIDs
|
|
476
606
|
pids = result.output.trim().split('\n');
|
|
477
607
|
}
|
|
478
|
-
|
|
608
|
+
|
|
479
609
|
for (const pid of pids) {
|
|
480
610
|
try {
|
|
481
611
|
if (isWindows) {
|
|
@@ -488,16 +618,22 @@ export class ProcessManager {
|
|
|
488
618
|
// Unix/Linux/macOS: use process.kill
|
|
489
619
|
process.kill(parseInt(pid), 'SIGKILL');
|
|
490
620
|
}
|
|
491
|
-
this.logger.verbose(
|
|
621
|
+
this.logger.verbose(
|
|
622
|
+
`- Killed process (PID: ${pid}) using port ${port}`,
|
|
623
|
+
);
|
|
492
624
|
} catch (killError) {
|
|
493
625
|
if (killError.code !== 'ESRCH') {
|
|
494
|
-
this.logger.error(
|
|
626
|
+
this.logger.error(
|
|
627
|
+
`- Failed to kill process (PID: ${pid}): ${killError.message}`,
|
|
628
|
+
);
|
|
495
629
|
}
|
|
496
630
|
}
|
|
497
631
|
}
|
|
498
632
|
}
|
|
499
633
|
} catch (portError) {
|
|
500
|
-
this.logger.error(
|
|
634
|
+
this.logger.error(
|
|
635
|
+
`- Failed to find process using port ${port}: ${portError.message}`,
|
|
636
|
+
);
|
|
501
637
|
}
|
|
502
638
|
}
|
|
503
639
|
} catch (error) {
|
|
@@ -508,10 +644,15 @@ export class ProcessManager {
|
|
|
508
644
|
// Final attempt to kill the process group
|
|
509
645
|
try {
|
|
510
646
|
const isWindows = process.platform === 'win32';
|
|
511
|
-
|
|
647
|
+
|
|
512
648
|
if (isWindows) {
|
|
513
649
|
// Windows: force kill with taskkill
|
|
514
|
-
const killProcess = spawn('taskkill', [
|
|
650
|
+
const killProcess = spawn('taskkill', [
|
|
651
|
+
'/F',
|
|
652
|
+
'/T',
|
|
653
|
+
'/PID',
|
|
654
|
+
pgid.toString(),
|
|
655
|
+
]);
|
|
515
656
|
await new Promise((resolve) => {
|
|
516
657
|
killProcess.on('close', resolve);
|
|
517
658
|
});
|
|
@@ -528,4 +669,4 @@ export class ProcessManager {
|
|
|
528
669
|
}
|
|
529
670
|
|
|
530
671
|
// For backward compatibility
|
|
531
|
-
export const processManager = new ProcessManager();
|
|
672
|
+
export const processManager = new ProcessManager();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scripts-orchestrator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "A powerful script orchestrator for running parallel commands with dependency management, background processes, and health checks",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"type": "module",
|