go-dev 0.4.2 → 0.6.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.
@@ -1,307 +1,359 @@
1
- const { spawn, spawnSync, execSync } = require('child_process');
2
- const prefixLines = require("./prefix-lines");
3
-
4
- const isWindows = process.platform === 'win32';
5
-
6
- class ProcessManager {
7
- constructor() {
8
- this.managedProcesses = new Set();
9
- this.cleanupInProgress = false;
10
- }
11
-
12
- /**
13
- * Runs a command synchronously (blocking).
14
- * Used for pre-commands, status checks, getting container names.
15
- * @param {string} command - The command to execute.
16
- * @param {string[]} args - Arguments for the command.
17
- * @param {object} [options={}] - Options for spawnSync (e.g., cwd, stdio).
18
- * @returns {string} The stdout of the command, trimmed.
19
- * @throws {Error} If the command fails.
20
- */
21
- runSync(command, args = [], options = {}) {
22
- if (this.cleanupInProgress) {
23
- console.warn(`[ProcessManager] Skipping synchronous command '${command}' during cleanup.`);
24
- return '';
25
- }
26
- console.log(`[ProcessManager] Running sync: ${command} ${args.join(' ')}`);
27
- try {
28
- const result = spawnSync(command, args, {
29
- shell: true,
30
- encoding: 'utf8',
31
- ...options,
32
- });
33
-
34
- if (result.error) {
35
- throw result.error;
36
- }
37
- if (result.status !== 0) {
38
- const stderrOutput = result.stderr ? result.stderr.trim() : 'No stderr output.';
39
- throw new Error(
40
- `Command failed with code ${result.status}: ${command} ${args.join(
41
- ' ',
42
- )}\n${stderrOutput}`,
43
- );
44
- }
45
- return result.stdout?.trim() ?? '';
46
- } catch (error) {
47
- throw new Error(`Failed to run sync command '${command}': ${error.message}`);
48
- }
49
- }
50
-
51
- /**
52
- * Runs a command asynchronously, inheriting stdio.
53
- * Used for initial 'docker compose up -d' where we want to see immediate output
54
- * and the process is not meant to be continually managed/restarted by the orchestrator.
55
- * @param {string} command - The command to execute.
56
- * @param {string[]} args - Arguments for the command.
57
- * @param {object} [options={}] - Options for spawn.
58
- * @returns {Promise<void>} A promise that resolves when the process exits successfully.
59
- */
60
- runInherited(command, args = [], options = {}) {
61
- return new Promise((resolve, reject) => {
62
- if (this.cleanupInProgress) {
63
- console.warn(`[ProcessManager] Skipping inherited command '${command}' during cleanup.`);
64
- return resolve();
65
- }
66
- console.log(`[ProcessManager] Running inherited: ${command} ${args.join(' ')}`);
67
- const proc = spawn(command, args, {
68
- shell: true,
69
- stdio: 'inherit',
70
- ...options,
71
- });
72
-
73
- proc.on('error', (err) => {
74
- console.error(`[ProcessManager] Failed to start inherited command '${command}':`, err);
75
- reject(err);
76
- });
77
-
78
- proc.on('exit', (code) => {
79
- if (code !== 0) {
80
- reject(
81
- new Error(`Inherited command '${command}' exited with code ${code}`),
82
- );
83
- } else {
84
- resolve();
85
- }
86
- });
87
- });
88
- }
89
-
90
- /**
91
- * Starts a long-running, managed process (like 'npx rollup -w').
92
- * Its output is prefixed, and it can be configured to restart on exit.
93
- * @param {string} command - The command to execute.
94
- * @param {string[]} args - Arguments for the command.
95
- * @param {object} options - Options for spawn (e.g., cwd).
96
- * @param {string} prefix - Prefix for stdout/stderr lines (e.g., 'frontend:').
97
- * @param {boolean} restartOnError - Whether to restart the process if it exits with non-zero code.
98
- * @param {Function} onExit
99
- * @param {ChildProcess[] | undefined} processReference - An array where to save the process to gain a reference to it.
100
- * @returns {ChildProcess} The spawned child process instance.
101
- */
102
- startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference) {
103
- if (this.cleanupInProgress) {
104
- console.warn(`[ProcessManager] Skipping managed process '${command}' during cleanup.`);
105
- return null;
106
- }
107
- console.log(
108
- `[ProcessManager] Starting managed process: ${command} ${args.join(
109
- ' ',
110
- )} (prefix: ${prefix})`,
111
- );
112
-
113
- if (processReference == null) {
114
- processReference = {}
115
- }
116
-
117
- const startedProcess = spawn(command, args, {
118
- shell: true,
119
- stdio: 'pipe',
120
- ...options,
121
- });
122
- processReference.process = startedProcess;
123
-
124
- let lastFormatting = '';
125
- startedProcess.stdout.on('data', (data) => {
126
- const result = prefixLines(data.toString(), prefix, lastFormatting);
127
- lastFormatting = result.lastFormatting;
128
- process.stdout.write(result.prefixedText);
129
- });
130
- startedProcess.stderr.on('data', (data) => {
131
- const result = prefixLines(data.toString(), prefix, lastFormatting);
132
- lastFormatting = result.lastFormatting;
133
- process.stderr.write(result.prefixedText);
134
- });
135
-
136
- this.managedProcesses.add(startedProcess);
137
-
138
- startedProcess.on('error', (err) => {
139
- console.error(
140
- `[ProcessManager] Error starting managed process '${command}': ${err.message}`,
141
- );
142
- this.managedProcesses.delete(startedProcess);
143
- });
144
-
145
- startedProcess.on('exit', (code) => {
146
- if (!this.managedProcesses.has(startedProcess)) {
147
- return;
148
- }
149
-
150
- this.managedProcesses.delete(startedProcess);
151
- if (this.cleanupInProgress) {
152
- console.log(
153
- `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited due to cleanup.`,
154
- );
155
- return;
156
- }
157
-
158
- if (code !== 0) {
159
- console.error(
160
- `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited with code ${code}.`,
161
- );
162
- if (restartOnError) {
163
- console.warn(
164
- `[ProcessManager] Restarting managed process '${command}'...`,
165
- );
166
- this.startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference);
167
- } else {
168
- onExit?.();
169
- }
170
- } else {
171
- console.log(
172
- `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited cleanly.`,
173
- );
174
- onExit?.();
175
- }
176
- });
177
-
178
- return processReference;
179
- }
180
-
181
- /**
182
- * Kills a single child process.
183
- * On Windows, uses taskkill to kill the entire process tree.
184
- * @param {ChildProcess} childProcess - The process to kill.
185
- * @returns {Promise<void>}
186
- */
187
- killProcess(childProcess) {
188
- if (childProcess.killed || childProcess.exitCode != null) {
189
- return Promise.resolve();
190
- }
191
-
192
- this.managedProcesses.delete(childProcess);
193
-
194
- if (isWindows) {
195
- try {
196
- execSync(`taskkill /T /F /PID ${childProcess.pid}`, { stdio: 'ignore' });
197
- } catch {
198
- // Process may have already been terminated by Ctrl+C signal propagation - this is OK
199
- }
200
- return Promise.resolve();
201
- }
202
-
203
- // On Unix, use SIGTERM with timeout fallback to SIGKILL
204
- return new Promise((resolve) => {
205
- let exited = false;
206
- let timeoutId = null;
207
-
208
- const onExit = () => {
209
- exited = true;
210
- if (timeoutId) {
211
- clearTimeout(timeoutId);
212
- timeoutId = null;
213
- }
214
- resolve();
215
- };
216
- childProcess.on('exit', onExit);
217
-
218
- timeoutId = setTimeout(() => {
219
- if (exited) {
220
- return;
221
- }
222
-
223
- console.error(`[ProcessManager] Timeout reached for process interruption ${childProcess.pid}`);
224
- try {
225
- childProcess.kill('SIGKILL');
226
- } catch (e) {
227
- console.error(`[ProcessManager] Error force killing process ${childProcess.pid}: ${e.message}`);
228
- }
229
- resolve();
230
- }, 500);
231
-
232
- try {
233
- childProcess.kill('SIGTERM');
234
- } catch (e) {
235
- console.error(`[ProcessManager] Error signaling process ${childProcess.pid}: ${e.message}`);
236
- if (timeoutId) {
237
- clearTimeout(timeoutId);
238
- }
239
- resolve();
240
- }
241
- });
242
- }
243
-
244
- /**
245
- * Kills all currently managed child processes.
246
- */
247
- async cleanupManagedProcesses() {
248
- if (this.cleanupInProgress) {
249
- console.log('[ProcessManager] Cleanup of managed processes already in progress, skipping.');
250
- return;
251
- }
252
-
253
- this.cleanupInProgress = true;
254
-
255
- console.log('\n[ProcessManager] Initiating cleanup of managed processes...');
256
-
257
- // Filter out already-dead processes
258
- const processesToKill = [...this.managedProcesses].filter(proc => {
259
- if (proc.killed || proc.exitCode != null) {
260
- this.managedProcesses.delete(proc);
261
- return false;
262
- }
263
- return true;
264
- });
265
-
266
- for (const proc of processesToKill) {
267
- console.log(`[ProcessManager] Killing managed process PID: ${proc.pid}`);
268
- try {
269
- if (isWindows) {
270
- // On Windows, use taskkill to kill the entire process tree
271
- try {
272
- execSync(`taskkill /T /F /PID ${proc.pid}`, { stdio: 'ignore' });
273
- } catch {
274
- // Process may have already been terminated - this is OK
275
- }
276
- } else {
277
- proc.kill('SIGTERM');
278
- }
279
- } catch (e) {
280
- console.error(`[ProcessManager] Error killing process ${proc.pid}: ${e.message}`);
281
- }
282
- }
283
-
284
- // On Unix, wait for processes to exit gracefully before using SIGKILL
285
- if (processesToKill.length > 0 && !isWindows) {
286
- await new Promise(resolve => setTimeout(resolve, 500));
287
-
288
- for (const proc of [...this.managedProcesses]) {
289
- if (proc.killed || proc.exitCode != null) {
290
- this.managedProcesses.delete(proc);
291
- continue;
292
- }
293
-
294
- console.warn(`[ProcessManager] Process ${proc.pid} did not exit gracefully, forcing kill.`);
295
- try {
296
- proc.kill('SIGKILL');
297
- } catch (e) {
298
- console.error(`[ProcessManager] Error forcing kill for process ${proc.pid}: ${e.message}`);
299
- }
300
- }
301
- }
302
-
303
- console.log('[ProcessManager] Managed process cleanup complete.');
304
- }
305
- }
306
-
1
+ const { spawn, spawnSync, execSync } = require('child_process');
2
+ const log = require('./logger');
3
+ const prefixLines = require("./prefix-lines");
4
+
5
+ const isWindows = process.platform === 'win32';
6
+
7
+ class ProcessManager {
8
+ constructor() {
9
+ this.managedProcesses = new Set();
10
+ this.cleanupInProgress = false;
11
+ }
12
+
13
+ /**
14
+ * Runs a command synchronously (blocking).
15
+ * Used for pre-commands, status checks, getting container names.
16
+ * @param {string} command - The command to execute.
17
+ * @param {string[]} args - Arguments for the command.
18
+ * @param {object} [options={}] - Options for spawnSync (e.g., cwd, stdio).
19
+ * @returns {string} The stdout of the command, trimmed.
20
+ * @throws {Error} If the command fails.
21
+ */
22
+ runSync(command, args = [], options = {}) {
23
+ if (this.cleanupInProgress) {
24
+ log.warn(`[ProcessManager] Skipping synchronous command '${command}' during cleanup.`);
25
+ return '';
26
+ }
27
+ log.debug(`[ProcessManager] Running sync: ${command} ${args.join(' ')}`);
28
+ try {
29
+ const result = spawnSync(command, args, {
30
+ shell: true,
31
+ encoding: 'utf8',
32
+ ...options,
33
+ });
34
+
35
+ if (result.error) {
36
+ throw result.error;
37
+ }
38
+ if (result.status !== 0) {
39
+ const stderrOutput = result.stderr ? result.stderr.trim() : 'No stderr output.';
40
+ throw new Error(
41
+ `Command failed with code ${result.status}: ${command} ${args.join(
42
+ ' ',
43
+ )}\n${stderrOutput}`,
44
+ );
45
+ }
46
+ return result.stdout?.trim() ?? '';
47
+ } catch (error) {
48
+ throw new Error(`Failed to run sync command '${command}': ${error.message}`);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Runs a command asynchronously, inheriting stdio.
54
+ * Used for initial 'docker compose up -d' where we want to see immediate output
55
+ * and the process is not meant to be continually managed/restarted by the orchestrator.
56
+ * @param {string} command - The command to execute.
57
+ * @param {string[]} args - Arguments for the command.
58
+ * @param {object} [options={}] - Options for spawn.
59
+ * @returns {Promise<void>} A promise that resolves when the process exits successfully.
60
+ */
61
+ runInherited(command, args = [], options = {}) {
62
+ return new Promise((resolve, reject) => {
63
+ if (this.cleanupInProgress) {
64
+ log.warn(`[ProcessManager] Skipping inherited command '${command}' during cleanup.`);
65
+ return resolve();
66
+ }
67
+ log.debug(`[ProcessManager] Running inherited: ${command} ${args.join(' ')}`);
68
+ const proc = spawn(command, args, {
69
+ shell: true,
70
+ stdio: 'inherit',
71
+ ...options,
72
+ });
73
+
74
+ proc.on('error', (err) => {
75
+ log.error(`[ProcessManager] Failed to start inherited command '${command}':`, err);
76
+ reject(err);
77
+ });
78
+
79
+ proc.on('exit', (code) => {
80
+ if (code !== 0) {
81
+ reject(
82
+ new Error(`Inherited command '${command}' exited with code ${code}`),
83
+ );
84
+ } else {
85
+ resolve();
86
+ }
87
+ });
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Runs a command asynchronously, prefixing its output like a managed process.
93
+ * Used for pre-commands (literal or service-as-preCommand) so their output is
94
+ * attributed to the owning service instead of leaking raw to the terminal.
95
+ * @param {string} command - The command to execute.
96
+ * @param {string[]} args - Arguments for the command.
97
+ * @param {object} [options={}] - Options for spawn (e.g., cwd).
98
+ * @param {string} prefix - Prefix for stdout/stderr lines.
99
+ * @returns {Promise<void>} Resolves when the process exits successfully.
100
+ */
101
+ runInheritedPrefixed(command, args = [], options = {}, prefix) {
102
+ return new Promise((resolve, reject) => {
103
+ if (this.cleanupInProgress) {
104
+ log.warn(`[ProcessManager] Skipping inherited command '${command}' during cleanup.`);
105
+ return resolve();
106
+ }
107
+ log.debug(`[ProcessManager] Running inherited (prefixed): ${command} ${args.join(' ')}`);
108
+ const proc = spawn(command, args, {
109
+ shell: true,
110
+ stdio: 'pipe',
111
+ ...options,
112
+ env: { FORCE_COLOR: '1', ...process.env, ...(options.env ?? {}) },
113
+ });
114
+
115
+ let lastFormatting = '';
116
+ const writePrefixed = (data, target) => {
117
+ const result = prefixLines(data.toString(), prefix, lastFormatting);
118
+ lastFormatting = result.lastFormatting;
119
+ target.write(result.prefixedText);
120
+ };
121
+ proc.stdout.on('data', (data) => writePrefixed(data, process.stdout));
122
+ proc.stderr.on('data', (data) => writePrefixed(data, process.stderr));
123
+
124
+ proc.on('error', (err) => {
125
+ log.error(`[ProcessManager] Failed to start inherited command '${command}':`, err);
126
+ reject(err);
127
+ });
128
+
129
+ proc.on('exit', (code) => {
130
+ if (code !== 0) {
131
+ reject(
132
+ new Error(`Inherited command '${command}' exited with code ${code}`),
133
+ );
134
+ } else {
135
+ resolve();
136
+ }
137
+ });
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Starts a long-running, managed process (like 'npx rollup -w').
143
+ * Its output is prefixed, and it can be configured to restart on exit.
144
+ * @param {string} command - The command to execute.
145
+ * @param {string[]} args - Arguments for the command.
146
+ * @param {object} options - Options for spawn (e.g., cwd).
147
+ * @param {string} prefix - Prefix for stdout/stderr lines (e.g., 'frontend:').
148
+ * @param {boolean} restartOnError - Whether to restart the process if it exits with non-zero code.
149
+ * @param {Function} onExit
150
+ * @param {ChildProcess[] | undefined} processReference - An array where to save the process to gain a reference to it.
151
+ * @returns {ChildProcess} The spawned child process instance.
152
+ */
153
+ startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference) {
154
+ if (this.cleanupInProgress) {
155
+ log.warn(`[ProcessManager] Skipping managed process '${command}' during cleanup.`);
156
+ return null;
157
+ }
158
+ log.debug(
159
+ `[ProcessManager] Starting managed process: ${command} ${args.join(
160
+ ' ',
161
+ )} (prefix: ${prefix})`,
162
+ );
163
+
164
+ if (processReference == null) {
165
+ processReference = {}
166
+ }
167
+
168
+ const startedProcess = spawn(command, args, {
169
+ shell: true,
170
+ stdio: 'pipe',
171
+ ...options,
172
+ env: { FORCE_COLOR: '1', ...process.env, ...(options.env ?? {}) },
173
+ });
174
+ processReference.process = startedProcess;
175
+
176
+ let lastFormatting = '';
177
+ startedProcess.stdout.on('data', (data) => {
178
+ const result = prefixLines(data.toString(), prefix, lastFormatting);
179
+ lastFormatting = result.lastFormatting;
180
+ process.stdout.write(result.prefixedText);
181
+ });
182
+ startedProcess.stderr.on('data', (data) => {
183
+ const result = prefixLines(data.toString(), prefix, lastFormatting);
184
+ lastFormatting = result.lastFormatting;
185
+ process.stderr.write(result.prefixedText);
186
+ });
187
+
188
+ this.managedProcesses.add(startedProcess);
189
+
190
+ startedProcess.on('error', (err) => {
191
+ log.error(
192
+ `[ProcessManager] Error starting managed process '${command}': ${err.message}`,
193
+ );
194
+ this.managedProcesses.delete(startedProcess);
195
+ });
196
+
197
+ startedProcess.on('exit', (code) => {
198
+ if (!this.managedProcesses.has(startedProcess)) {
199
+ return;
200
+ }
201
+
202
+ this.managedProcesses.delete(startedProcess);
203
+ if (this.cleanupInProgress) {
204
+ log.debug(
205
+ `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited due to cleanup.`,
206
+ );
207
+ return;
208
+ }
209
+
210
+ if (code !== 0) {
211
+ log.error(
212
+ `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited with code ${code}.`,
213
+ );
214
+ if (restartOnError) {
215
+ log.warn(
216
+ `[ProcessManager] Restarting managed process '${command}'...`,
217
+ );
218
+ this.startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference);
219
+ } else {
220
+ onExit?.();
221
+ }
222
+ } else {
223
+ log.debug(
224
+ `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited cleanly.`,
225
+ );
226
+ onExit?.();
227
+ }
228
+ });
229
+
230
+ return processReference;
231
+ }
232
+
233
+ /**
234
+ * Kills a single child process.
235
+ * On Windows, uses taskkill to kill the entire process tree.
236
+ * @param {ChildProcess} childProcess - The process to kill.
237
+ * @returns {Promise<void>}
238
+ */
239
+ killProcess(childProcess) {
240
+ if (childProcess.killed || childProcess.exitCode != null) {
241
+ return Promise.resolve();
242
+ }
243
+
244
+ this.managedProcesses.delete(childProcess);
245
+
246
+ if (isWindows) {
247
+ try {
248
+ execSync(`taskkill /T /F /PID ${childProcess.pid}`, { stdio: 'ignore' });
249
+ } catch {
250
+ // Process may have already been terminated by Ctrl+C signal propagation - this is OK
251
+ }
252
+ return Promise.resolve();
253
+ }
254
+
255
+ // On Unix, use SIGTERM with timeout fallback to SIGKILL
256
+ return new Promise((resolve) => {
257
+ let exited = false;
258
+ let timeoutId = null;
259
+
260
+ const onExit = () => {
261
+ exited = true;
262
+ if (timeoutId) {
263
+ clearTimeout(timeoutId);
264
+ timeoutId = null;
265
+ }
266
+ resolve();
267
+ };
268
+ childProcess.on('exit', onExit);
269
+
270
+ timeoutId = setTimeout(() => {
271
+ if (exited) {
272
+ return;
273
+ }
274
+
275
+ log.error(`[ProcessManager] Timeout reached for process interruption ${childProcess.pid}`);
276
+ try {
277
+ childProcess.kill('SIGKILL');
278
+ } catch (e) {
279
+ log.error(`[ProcessManager] Error force killing process ${childProcess.pid}: ${e.message}`);
280
+ }
281
+ resolve();
282
+ }, 500);
283
+
284
+ try {
285
+ childProcess.kill('SIGTERM');
286
+ } catch (e) {
287
+ log.error(`[ProcessManager] Error signaling process ${childProcess.pid}: ${e.message}`);
288
+ if (timeoutId) {
289
+ clearTimeout(timeoutId);
290
+ }
291
+ resolve();
292
+ }
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Kills all currently managed child processes.
298
+ */
299
+ async cleanupManagedProcesses() {
300
+ if (this.cleanupInProgress) {
301
+ log.debug('[ProcessManager] Cleanup of managed processes already in progress, skipping.');
302
+ return;
303
+ }
304
+
305
+ this.cleanupInProgress = true;
306
+
307
+ log.debug('\n[ProcessManager] Initiating cleanup of managed processes...');
308
+
309
+ // Filter out already-dead processes
310
+ const processesToKill = [...this.managedProcesses].filter(proc => {
311
+ if (proc.killed || proc.exitCode != null) {
312
+ this.managedProcesses.delete(proc);
313
+ return false;
314
+ }
315
+ return true;
316
+ });
317
+
318
+ for (const proc of processesToKill) {
319
+ log.debug(`[ProcessManager] Killing managed process PID: ${proc.pid}`);
320
+ try {
321
+ if (isWindows) {
322
+ // On Windows, use taskkill to kill the entire process tree
323
+ try {
324
+ execSync(`taskkill /T /F /PID ${proc.pid}`, { stdio: 'ignore' });
325
+ } catch {
326
+ // Process may have already been terminated - this is OK
327
+ }
328
+ } else {
329
+ proc.kill('SIGTERM');
330
+ }
331
+ } catch (e) {
332
+ log.error(`[ProcessManager] Error killing process ${proc.pid}: ${e.message}`);
333
+ }
334
+ }
335
+
336
+ // On Unix, wait for processes to exit gracefully before using SIGKILL
337
+ if (processesToKill.length > 0 && !isWindows) {
338
+ await new Promise(resolve => setTimeout(resolve, 500));
339
+
340
+ for (const proc of [...this.managedProcesses]) {
341
+ if (proc.killed || proc.exitCode != null) {
342
+ this.managedProcesses.delete(proc);
343
+ continue;
344
+ }
345
+
346
+ log.warn(`[ProcessManager] Process ${proc.pid} did not exit gracefully, forcing kill.`);
347
+ try {
348
+ proc.kill('SIGKILL');
349
+ } catch (e) {
350
+ log.error(`[ProcessManager] Error forcing kill for process ${proc.pid}: ${e.message}`);
351
+ }
352
+ }
353
+ }
354
+
355
+ log.debug('[ProcessManager] Managed process cleanup complete.');
356
+ }
357
+ }
358
+
307
359
  module.exports = ProcessManager;