mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
217 lines
6.7 KiB
JavaScript
217 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,
|
|
});
|
|
|
|
// 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();
|
|
});
|
|
}
|