Files
meteor/packages/tools-core/lib/process.js

218 lines
6.7 KiB
JavaScript

const { spawn } = require('child_process');
const net = require('net');
/**
* Spawns a new OS process with the given command and arguments.
* Streams output with original styling and handles errors and exit events.
* Always preserves raw output formatting (colors, progress bars, etc.) and
* provides decoded string data to callbacks for logic/checking/logging.
*
* @param {string} command - The command to run
* @param {string[]} args - Arguments to pass to the command
* @param {Object} options - Options for the spawned process
* @param {Object} [options.env] - Environment variables to merge with process.env
* @param {string} [options.cwd] - Current working directory
* @param {boolean} [options.detached] - Whether to run the process detached from the parent
* @param {Function} [options.onStdout] - Callback for stdout data (receives decoded string)
* @param {Function} [options.onStderr] - Callback for stderr data (receives decoded string)
* @param {Function} [options.onExit] - Callback when process exits
* @param {Function} [options.onError] - Callback when process encounters an error
* @returns {Object} The spawned process with additional utility methods
*/
export function spawnProcess(command, args, options = {}) {
const proc = spawn(command, args, {
env: { ...process.env, ...(options.env || {}), FORCE_COLOR: '1', TERM: 'xterm-256color' },
cwd: options.cwd || process.cwd(),
stdio: ['pipe', 'pipe', 'pipe'],
detached: options.detached || false,
...(process.platform === 'win32' && { shell: true }),
});
// Add a reference to track if the process is running
proc.isRunning = true;
// Handle stdout
proc.stdout.on('data', (buf) => {
if (options.onStdout) {
options.onStdout(buf.toString());
}
});
// Handle stderr
proc.stderr.on('data', (buf) => {
if (options.onStderr) {
options.onStderr(buf.toString());
}
});
// Handle process exit
proc.on('close', (code, signal) => {
proc.isRunning = false;
if (options.onExit) options.onExit(code, signal);
});
// Handle process errors
proc.on('error', (err) => {
proc.isRunning = false;
if (options.onError) options.onError(err);
else console.error(`Process error: ${err.message}`);
});
// This happens sometimes when we write to stdin after the app
// is dead. If we don't register a handler, we get a top level
// exception and the whole app dies.
proc.stdin.on('error', () => {});
if (options.detached) proc.unref();
return proc;
}
/**
* Stops a running process.
*
* @param {Object} proc - The process to stop
* @param {Object} [options] - Options for stopping the process
* @param {string} [options.signal='SIGTERM'] - The signal to send to the process
* @param {number} [options.timeout=5000] - Timeout in ms before forcing kill with SIGKILL
* @returns {Promise<void>} A promise that resolves when the process is stopped
*/
export function stopProcess(proc, options = {}) {
if (!proc || !proc.pid || !isProcessRunning(proc)) {
return Promise.resolve();
}
const signal = options.signal || 'SIGTERM';
const timeout = options.timeout || 5000;
return new Promise((resolve) => {
// Set a timeout to force kill if the process doesn't exit gracefully
const forceKillTimeout = setTimeout(() => {
if (isProcessRunning(proc)) {
proc.kill('SIGKILL');
}
}, timeout);
// Listen for the process to exit
proc.on('close', () => {
clearTimeout(forceKillTimeout);
proc.isRunning = false;
resolve();
});
// Send the signal to terminate the process
proc.kill(signal);
});
}
/**
* Checks if a process is running.
*
* @param {Object} proc - The process to check
* @returns {boolean} True if the process is running, false otherwise
*/
export function isProcessRunning(proc) {
if (!proc || !proc.pid) {
return false;
}
// If we've been tracking the process state with our isRunning property
if (proc.isRunning === false) {
return false;
}
// Try to send signal 0 to the process, which doesn't actually send a signal
// but checks if the process exists
try {
process.kill(proc.pid, 0);
return true;
} catch (e) {
return false;
}
}
/**
* Checks if a port is available.
*
* @param {number} port - The port to check
* @param {string} [host='127.0.0.1'] - The host to check
* @returns {Promise<boolean>} A promise that resolves to true if the port is available, false otherwise
*/
export function isPortAvailable(port, host = '127.0.0.1') {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
resolve(false);
} else {
// For other errors, we'll assume the port is not available
resolve(false);
}
});
server.once('listening', () => {
// Close the server and resolve with true (port is available)
server.close(() => {
resolve(true);
});
});
server.listen(port, host);
});
}
/**
* Waits for a port to become available or unavailable.
*
* @param {number} port - The port to check
* @param {Object} [options] - Options for waiting
* @param {string} [options.host='127.0.0.1'] - The host to check
* @param {boolean} [options.waitUntilAvailable=false] - If true, wait until port is available; if false, wait until port is in use
* @param {number} [options.timeout=30000] - Timeout in ms
* @param {number} [options.interval=500] - Interval between checks in ms
* @returns {Promise<boolean>} A promise that resolves to true if the condition is met, false if timed out
*/
export function waitForPort(port, options = {}) {
const host = options.host || '127.0.0.1';
const waitUntilAvailable = options.waitUntilAvailable || false;
const timeout = options.timeout || 30000;
const interval = options.interval || 500;
const startTime = Date.now();
return new Promise((resolve) => {
let timeoutId = null;
const check = async () => {
// Check if we've exceeded the timeout
if (Date.now() - startTime > timeout) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
resolve(false);
return;
}
const isAvailable = await isPortAvailable(port, host);
// If we're waiting for the port to be available and it is, or
// if we're waiting for the port to be in use and it's not available
if ((waitUntilAvailable && isAvailable) || (!waitUntilAvailable && !isAvailable)) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
resolve(true);
return;
}
// Schedule the next check
timeoutId = setTimeout(check, interval);
};
// Start checking
check();
});
}