/
githubmirror
/
meteor
Обзор
Документация
Войти
/
githubmirror
/
meteor
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
devel
packages/tools-core/lib/process.js
270 строк
9 KB
Italo José
Release 3.5.1 ⌛ (#14555)
06 авг 2026, 02:08
Не верифицирован
06 авг 2026, 02:08
efef960
Код
Авторство
О чём код?
const { spawn } = require('child_process'); const net = require('net'); const { logError } = require('./log'); /** * Determines whether a spawned command should run through a shell. * * On Windows, batch launchers such as .cmd and .bat require a shell, while * direct executables should avoid shell parsing so argument boundaries are * preserved. * * @param {string} command - The command to evaluate * @param {Object} [options] - Spawn options * @param {boolean} [options.shell] - Explicit shell override * @returns {boolean} True if the command should run through a shell */ export function shouldUseShell(command, options = {}) { if (typeof options.shell === 'boolean') { return options.shell; } return process.platform === 'win32' && /\.(cmd|bat)$/i.test(command); } /** * 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, shell: shouldUseShell(command, options), }); // Track detached spawns so stopProcess can signal the whole process group. // POSIX-only: kernels deliver `proc.kill(signal)` to the immediate child PID, // but wrappers like npx don't reliably forward that signal to the real binary // they exec'd, leaving the grandchild (e.g. rspack devserver) holding ports. proc.meteorDetached = options.detached === 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 logError(`=> 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)) { sendSignal(proc, 'SIGKILL'); } }, timeout); // Listen for the process to exit proc.on('close', () => { clearTimeout(forceKillTimeout); proc.isRunning = false; resolve(); }); // Send the signal to terminate the process sendSignal(proc, signal); }); } /** * Sends a signal to a child process. For detached children on POSIX, signals * the whole process group (negative PID) so wrappers like npx propagate the * signal to the real binary they spawned. Falls back to a direct PID signal * if the group signal fails or on Windows. * * @param {Object} proc - The child process to signal * @param {string} signal - The signal name (e.g. 'SIGTERM', 'SIGKILL') * @returns {void} */ export function sendSignal(proc, signal) { if (proc.meteorDetached && process.platform !== 'win32') { try { process.kill(-proc.pid, signal); return; } catch (e) { // ESRCH means group is already gone; otherwise fall through to direct kill. } } try { proc.kill(signal); } catch (e) { // Best-effort: child may have already exited. } } /** * 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(); }); }