Пошук уроків, статей та іншого контенту
Запускайте зовнішні команди й дочірні процеси, керуйте їхнім життєвим циклом та обробляйте результати.
Дочірній процес — це окремий процес операційної системи, який запускається з Node.js. Він може виконувати:
системні команди;
виконувані файли;
скрипти;
інший файл Node.js.
Для роботи з дочірніми процесами Node.js має вбудований модуль node:child_process. Додаткові пакети не потрібні.
Дочірній процес має власний життєвий цикл і стандартні потоки:
stdin — вхідні дані;
stdout — звичайний вивід;
stderr — повідомлення про помилки;
код завершення;
сигнал, якщо процес було перервано сигналом операційної системи.
Модуль node:child_process надає кілька способів запуску процесів.
spawn()spawn() запускає процес і повертає об'єкт, з якого можна читати потоки в реальному часі.
import { spawn } from 'node:child_process';
const child = spawn('node', ['-e', 'console.log("Привіт із дочірнього процесу")']);
child.stdout.on('data', (data) => {
process.stdout.write(`stdout: ${data}`);
});
child.stderr.on('data', (data) => {
process.stderr.write(`stderr: ${data}`);
});
child.on('error', (error) => {
console.error('Не вдалося запустити процес:', error.message);
});
child.on('close', (code, signal) => {
console.log(`Процес завершено. Код: ${code}, сигнал: ${signal ?? 'немає'}`);
});Аргументи передаються окремим масивом:
spawn('node', ['script.js', '--port', '3000']);Такий підхід зручний, коли:
команда може працювати довго;
потрібно обробляти великі обсяги виводу;
результат потрібно отримувати частинами;
необхідно керувати процесом під час його роботи.
exec()exec() запускає команду через shell і накопичує весь вивід у пам'яті. Результат передається в callback.
import { exec } from 'node:child_process';
exec('node -e "console.log(2 + 3)"', (error, stdout, stderr) => {
if (error) {
console.error('Помилка запуску:', error.message);
return;
}
if (stderr) {
console.error('Повідомлення stderr:', stderr);
}
console.log('Результат:', stdout.trim());
});exec() зручний для коротких команд із невеликим виводом. Оскільки команда виконується через shell, потрібно обережно працювати з даними, що надходять від користувача.
Небезпечний приклад:
import { exec } from 'node:child_process';
const fileName = userInput;
// Не робіть так: userInput може містити shell-команди.
exec(`cat ${fileName}`, callback);Безпечніший варіант — execFile() або spawn() з масивом аргументів.
execFile()execFile() запускає безпосередньо виконуваний файл, не створюючи shell за замовчуванням.
import { execFile } from 'node:child_process';
execFile('node', ['-p', '6 * 7'], (error, stdout, stderr) => {
if (error) {
console.error('Помилка:', error.message);
return;
}
console.log('Результат:', stdout.trim());
if (stderr) {
console.error('stderr:', stderr);
}
});Цей метод добре підходить, коли:
відомо, який виконуваний файл потрібно запустити;
аргументи можна передати окремим масивом;
не потрібні можливості shell;
потрібно зменшити ризик shell-ін'єкції.
fork()fork() — спеціалізований спосіб запуску іншого JavaScript-файлу Node.js. На відміну від spawn(), такий процес отримує канал для обміну повідомленнями через send() та подію message.
Файл worker.js:
process.on('message', (message) => {
if (message.type !== 'sum') {
process.send?.({
type: 'error',
message: 'Невідома операція',
});
return;
}
const result = message.values.reduce((sum, value) => sum + value, 0);
process.send?.({
type: 'result',
value: result,
});
});Основний файл:
import { fork } from 'node:child_process';
const worker = fork('./worker.js');
worker.on('message', (message) => {
if (message.type === 'result') {
console.log('Сума:', message.value);
worker.disconnect();
}
});
worker.on('error', (error) => {
console.error('Помилка дочірнього процесу:', error.message);
});
worker.on('exit', (code, signal) => {
console.log(`Worker завершено. Код: ${code}, сигнал: ${signal ?? 'немає'}`);
});
worker.send({
type: 'sum',
values: [10, 20, 30],
});fork() використовують саме для взаємодії між Node.js-процесами. Він не призначений для запуску довільних системних команд.
У spawn() об'єкт дочірнього процесу має властивості:
child.stdin;
child.stdout;
child.stderr.
Ці властивості є потоками Node.js.
import { spawn } from 'node:child_process';
const child = spawn('node', [
'-e',
`
console.log('Рядок 1');
setTimeout(() => console.log('Рядок 2'), 100);
`,
]);
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
console.log('Отримано частину виводу:', chunk);
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk) => {
console.error('Помилка дочірнього процесу:', chunk);
});
child.on('close', (code) => {
console.log('Потоки закрито, код завершення:', code);
});Подія data може спрацьовувати багато разів. Не можна припускати, що один виклик data міститиме весь рядок або весь результат.
stdinДані можна передати процесу через його стандартний вхід:
import { spawn } from 'node:child_process';
const child = spawn('node', [
'-e',
`
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
input += chunk;
});
process.stdin.on('end', () => {
console.log(input.toUpperCase());
});
`,
]);
child.stdout.pipe(process.stdout);
child.stdin.write('текст від батьківського процесу');
child.stdin.end();Після завершення передавання потрібно викликати child.stdin.end(). Інакше дочірній процес може чекати на нові дані й не завершитися.
Під час роботи з дочірніми процесами важливо розрізняти події error, exit і close.
errorerror виникає, коли Node.js не зміг запустити процес або не зміг виконати операцію з ним.
Наприклад, команда може не існувати:
import { spawn } from 'node:child_process';
const child = spawn('command-that-does-not-exist');
child.on('error', (error) => {
console.error('Запуск не вдався:', error.code);
});Помилка запуску не означає те саме, що ненульовий код завершення. Якщо команда запустилася, але завершилася з помилкою, зазвичай потрібно перевіряти код завершення.
exitexit спрацьовує, коли основний процес завершився. Обробник отримує:
code — код завершення або null;
signal — сигнал або null.
child.on('exit', (code, signal) => {
if (code === 0) {
console.log('Успішне завершення');
} else {
console.error('Процес завершено з помилкою:', { code, signal });
}
});Зазвичай код 0 означає успішне завершення, а ненульовий код — помилку. Точне значення кодів залежить від конкретної програми.
closeclose спрацьовує після закриття потоків stdio. Якщо потрібно обробити весь вивід процесу, зазвичай краще орієнтуватися саме на цю подію.
child.on('close', (code, signal) => {
console.log('Процес і його потоки повністю завершено');
});exit та close можуть мати різні моменти спрацюванняПроцес може завершитися раніше, ніж будуть закриті його стандартні потоки. Тому:
exit повідомляє про завершення процесу;
close повідомляє про завершення процесу та закриття потоків.
Не варто використовувати обидві події для виконання однієї й тієї самої фінальної дії без додаткового захисту від повторного виконання.
Методи exec() та execFile() мають Promise-версії в модулі node:child_process.
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
try {
const { stdout, stderr } = await execFileAsync('node', ['-p', '10 + 5']);
console.log('Результат:', stdout.trim());
if (stderr) {
console.error('stderr:', stderr);
}
} catch (error) {
console.error('Команда завершилася з помилкою:', error.message);
}Якщо команда завершується з ненульовим кодом, Promise відхиляється. Це дає змогу використовувати звичайний try...catch.
Для spawn() зручніше створити власну Promise-обгортку, яка збирає вивід і чекає на close.
import { spawn } from 'node:child_process';
function run(command, args = []) {
return new Promise((resolve, reject) => {
const child = spawn(command, args);
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
stdout += chunk;
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
});
child.on('error', reject);
child.on('close', (code, signal) => {
if (code === 0) {
resolve({ stdout, stderr });
return;
}
const error = new Error(
`Процес завершено невдало: code=${code}, signal=${signal ?? 'немає'}`
);
error.code = code;
error.signal = signal;
error.stderr = stderr;
reject(error);
});
});
}
try {
const result = await run('node', ['-p', '4 * 8']);
console.log('Результат:', result.stdout.trim());
} catch (error) {
console.error(error.message);
}Збирання всього виводу в рядок підходить лише для результатів обмеженого розміру. Для великих результатів краще обробляти дані частинами або спрямовувати потік у файл чи інший потік.
Метод child.kill() надсилає дочірньому процесу сигнал.
import { spawn } from 'node:child_process';
const child = spawn('node', [
'-e',
'setInterval(() => console.log("працюю"), 200)',
]);
child.stdout.pipe(process.stdout);
const timer = setTimeout(() => {
console.log('Зупиняємо процес');
child.kill('SIGTERM');
}, 1000);
child.on('close', (code, signal) => {
clearTimeout(timer);
console.log('Завершено:', { code, signal });
});Поширені сигнали:
SIGTERM — прохання коректно завершити роботу;
SIGKILL — примусове завершення;
SIGINT — переривання, аналогічне натисканню Ctrl+C у терміналі.
Бажано спочатку використовувати SIGTERM, щоб програма мала змогу звільнити ресурси. SIGKILL не дає процесу виконати код завершення.
Виклик kill() не гарантує, що процес уже завершився. Фактичне завершення потрібно обробляти через close або exit.
Для spawn() можна самостійно встановити таймер і завершити процес, якщо він працює надто довго.
import { spawn } from 'node:child_process';
function runWithTimeout(command, args, timeoutMs) {
return new Promise((resolve, reject) => {
const child = spawn(command, args);
let finished = false;
const timer = setTimeout(() => {
if (!finished) {
child.kill('SIGTERM');
}
}, timeoutMs);
child.on('error', (error) => {
finished = true;
clearTimeout(timer);
reject(error);
});
child.on('close', (code, signal) => {
finished = true;
clearTimeout(timer);
if (signal === 'SIGTERM') {
reject(new Error('Процес перевищив обмеження часу'));
return;
}
resolve({ code, signal });
});
});
}
try {
await runWithTimeout(
'node',
['-e', 'setTimeout(() => console.log("готово"), 5000)'],
1000
);
} catch (error) {
console.error(error.message);
}У реальному застосунку також потрібно враховувати дочірні процеси, які запускають власні дочірні процеси. Завершення одного процесу не завжди автоматично завершує все дерево процесів.
Третій аргумент spawn() — це об'єкт опцій.
import { spawn } from 'node:child_process';
const child = spawn('node', ['-e', 'console.log(process.cwd())'], {
cwd: '/tmp',
});
child.stdout.pipe(process.stdout);cwd визначає робочу директорію дочірнього процесу. Якщо вказано неіснуючий шлях, запуск завершиться помилкою.
import { spawn } from 'node:child_process';
const child = spawn('node', ['-e', 'console.log(process.env.APP_MODE)'], {
env: {
...process.env,
APP_MODE: 'production',
},
});
child.stdout.pipe(process.stdout);Під час створення нового об'єкта env важливо зберігати process.env, якщо дочірньому процесу потрібні наявні змінні середовища, зокрема PATH.
За замовчуванням потоки доступні через об'єкт child. Їхню поведінку можна змінити опцією stdio.
import { spawn } from 'node:child_process';
const child = spawn('node', ['-e', 'console.log("вивід")'], {
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);Позиції масиву stdio:
stdin;
stdout;
stderr.
Значення можуть бути:
'pipe' — створити потік;
'ignore' — ігнорувати;
'inherit' — використати потоки батьківського процесу.
Наприклад, щоб показувати вивід безпосередньо в тому самому терміналі:
import { spawn } from 'node:child_process';
spawn('node', ['-e', 'console.log("видно в терміналі")'], {
stdio: 'inherit',
});Опція shell: true запускає команду через shell:
import { spawn } from 'node:child_process';
const child = spawn('echo', ['Привіт'], {
shell: true,
});
child.stdout.pipe(process.stdout);Shell може бути потрібен для shell-синтаксису, перенаправлень або конвеєрів. Проте він небезпечний, якщо частина команди формується з неперевірених даних.
Не вставляйте користувацькі значення безпосередньо в командний рядок:
// Небезпечно: значення може містити додаткові shell-команди.
exec(`some-command ${userInput}`);Надавайте перевагу передачі аргументів окремим масивом:
import { spawn } from 'node:child_process';
const child = spawn('some-command', [userInput]);Це не замінює валідацію. Потрібно також перевіряти:
чи дозволене саме це значення;
чи відповідає воно очікуваному формату;
чи не перевищує допустимий розмір;
чи має процес необхідні права.
Наведений приклад запускає Node.js як дочірній процес, передає йому дані через змінну середовища, читає stdout і stderr, а також перевіряє код завершення.
import { spawn } from 'node:child_process';
const script = `
console.log('Отримано:', process.env.INPUT);
if (!process.env.INPUT) {
console.error('Вхідні дані відсутні');
process.exit(1);
}
`;
const child = spawn('node', ['-e', script], {
env: {
...process.env,
INPUT: 'дані від батьківського процесу',
},
});
let output = '';
let errors = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
output += chunk;
});
child.stderr.on('data', (chunk) => {
errors += chunk;
});
child.on('error', (error) => {
console.error('Не вдалося запустити дочірній процес:', error.message);
});
child.on('close', (code, signal) => {
if (signal) {
console.error(`Процес перервано сигналом ${signal}`);
return;
}
if (code !== 0) {
console.error(`Процес завершено з кодом ${code}`);
console.error(errors.trim());
return;
}
console.log('Успішний результат:', output.trim());
});exec() для великих результатівexec() накопичує результат у пам'яті. Великий вивід може перевищити ліміт буфера або створити надмірне споживання пам'яті.
Для потокового оброблення використовуйте spawn().
stderrНаявність даних у stderr не завжди означає, що команда провалилася. Деякі програми пишуть попередження або діагностичні повідомлення в stderr, але завершуються з кодом 0.
Основним показником успіху зазвичай є код завершення.
errorЯкщо процес не вдалося запустити, подія error має бути оброблена. Без належного обробника помилка може призвести до аварійного завершення батьківського процесу.
dataПотік може розділити дані на довільну кількість частин. Один фрагмент не обов'язково відповідає одному рядку або одному повідомленню.
stdinЯкщо дочірня програма читає до кінця stdin, після передавання даних потрібно викликати:
child.stdin.end();Конкатенація користувацьких значень із командою може призвести до виконання сторонніх команд. Передавайте аргументи масивом через spawn() або execFile() і виконуйте валідацію.
kill() без очікування завершенняchild.kill() лише надсилає сигнал. Ресурси вважайте звільненими після події close або exit.
Модуль node:child_process дає змогу запускати зовнішні команди та Node.js-процеси.
spawn() підходить для потокового читання виводу й довготривалих процесів.
exec() зручний для коротких команд із невеликим результатом, але працює через shell.
execFile() запускає виконуваний файл без shell за замовчуванням і є безпечнішим для окремих аргументів.
fork() призначений для запуску іншого Node.js-файлу та обміну повідомленнями між процесами.
Подія error описує проблеми запуску або взаємодії з процесом.
Подія exit повідомляє про завершення процесу, а close — про завершення процесу та його потоків.
Для зупинення процесу використовуйте child.kill() і перевіряйте фактичне завершення через події життєвого циклу.
Не вставляйте неперевірені дані в shell-команди. Передавайте аргументи окремим масивом і обмежуйте час та ресурси виконання.