Node.js Child Process API: اجرای فرآیندهای فرزند

ماژول node:child_process در Node.js امکان اجرای فرآیندهای فرزند (subprocesses) را فراهم می‌کند. این قابلیت مشابه popen(3) در سیستم‌های یونیکس است اما کاملاً یکسان نیست. مهم‌ترین تابع در این ماژول، child_process.spawn() است که فرآیند فرزند را به‌صورت ناهمزمان ایجاد می‌کند. علاوه بر آن، توابع دیگری مانند exec, execFile, fork و نسخه‌های همگام (Sync) نیز وجود دارند که بسته به نیاز می‌توانند استفاده شوند.

child_process.spawn / spawnSyncchild_process.exec / execSyncchild_process.execFile / execFileSyncchild_process.forkIPC Communication

~13 دقیقه مطالعه • بروزرسانی ۵ دی ۱۴۰۴

1. ایجاد فرآیند فرزند با spawn()


spawn() فرآیند فرزند را به‌صورت ناهمزمان ایجاد می‌کند و جریان‌های stdin, stdout, stderr بین والد و فرزند برقرار می‌شوند.

const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

2. مدیریت ورودی/خروجی


  • اگر خروجی مصرف نشود، فرآیند فرزند ممکن است بلوکه شود.
  • گزینه { stdio: 'ignore' } برای نادیده گرفتن خروجی استفاده می‌شود.

3. جایگزین‌های exec و execFile


  • exec(): اجرای دستور درون یک shell و بازگرداندن خروجی به callback.
  • execFile(): اجرای مستقیم فایل بدون ایجاد shell.
  • fork(): ایجاد یک فرآیند Node.js جدید با کانال IPC برای ارتباط.
  • نسخه‌های همگام مانند execSync() و execFileSync() حلقهٔ رویداد را بلوکه می‌کنند.

4. اجرای فایل‌های .bat و .cmd در ویندوز


  • در ویندوز، فایل‌های .bat و .cmd نیازمند shell هستند.
  • می‌توان از spawn() با گزینه { shell: true } یا exec() استفاده کرد.
const { exec } = require('node:child_process');
exec('my.bat', (err, stdout, stderr) => {
  if (err) console.error(err);
  console.log(stdout);
});

5. استفاده از exec با callback


const { exec } = require('node:child_process');
exec('cat *.js missing_file | wc -l', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

6. نسخهٔ Promise با util.promisify


const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);

async function lsExample() {
  const { stdout, stderr } = await exec('ls');
  console.log('stdout:', stdout);
  console.error('stderr:', stderr);
}
lsExample();

7. کنترل با AbortController


گزینهٔ signal اجازه می‌دهد فرآیند با AbortController متوقف شود.

const { exec } = require('node:child_process');
const controller = new AbortController();
const { signal } = controller;

const child = exec('grep ssh', { signal }, (error) => {
  console.error(error); // AbortError
});
controller.abort();

نتیجه‌گیری


ماژول child_process در Node.js ابزاری قدرتمند برای اجرای دستورات سیستم و مدیریت فرآیندهای فرزند است. با استفاده از روش‌های ناهمزمان مانند spawn() و exec() می‌توان عملکرد بهینه داشت، در حالی که نسخه‌های همگام برای اسکریپت‌های ساده مناسب‌تر هستند. همچنین، پشتیبانی از AbortController و IPC ارتباط بین فرآیندها را ایمن‌تر و انعطاف‌پذیرتر می‌کند.

1. execFile()


  • اجرای مستقیم فایل اجرایی بدون shell.
  • کارایی بالاتر نسبت به exec چون shell ایجاد نمی‌شود.
  • گزینه‌ها مشابه exec هستند اما قابلیت‌هایی مثل I/O redirection و globbing پشتیبانی نمی‌شوند.
const { execFile } = require('node:child_process');
execFile('node', ['--version'], (error, stdout, stderr) => {
  if (error) throw error;
  console.log(stdout);
});

نسخهٔ Promise با util.promisify نیز پشتیبانی می‌شود.

2. fork()


  • نسخهٔ ویژه‌ای از spawn برای ایجاد فرآیند Node.js جدید.
  • کانال ارتباطی IPC برای ارسال پیام بین والد و فرزند فراهم می‌کند.
  • هر فرآیند Node.js فرزند مستقل از والد است و حافظه و Isolate جداگانه دارد.
const { fork } = require('node:child_process');
const child = fork('child.js');
child.on('message', (msg) => {
  console.log('Message from child:', msg);
});
child.send({ hello: 'world' });

3. spawn()


  • ایجاد فرآیند جدید با دستور و آرگومان‌ها.
  • گزینه‌ها شامل cwd، env، stdio و غیره هستند.
  • در صورت فعال بودن گزینه shell، باید ورودی کاربر پاک‌سازی شود تا از اجرای دستورات ناخواسته جلوگیری شود.
const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

4. مدیریت خطا و AbortController


  • هر سه تابع از گزینهٔ signal پشتیبانی می‌کنند.
  • با استفاده از AbortController می‌توان فرآیند را متوقف کرد.
const controller = new AbortController();
const { signal } = controller;
const child = spawn('grep', ['ssh'], { signal });
controller.abort(); // توقف فرآیند

نتیجه‌گیری


توابع execFile، fork و spawn ابزارهای قدرتمندی برای مدیریت فرآیندهای فرزند در Node.js هستند. با استفادهٔ صحیح از این توابع می‌توان برنامه‌های مقیاس‌پذیر و انعطاف‌پذیر ساخت که از قابلیت‌های سیستم‌عامل و ارتباط بین فرآیندها بهره‌مند می‌شوند.

1. رفتار در ویندوز


  • با تنظیم detached: true، فرآیند فرزند پس از خروج والد همچنان اجرا می‌شود.
  • فرآیند فرزند یک پنجرهٔ کنسول مستقل خواهد داشت.
  • پس از فعال‌سازی، این ویژگی قابل غیرفعال‌سازی نیست.

2. رفتار در سیستم‌های شبه‌یونیکس


  • فرآیند فرزند رهبر یک گروه و نشست جدید می‌شود (setsid(2)).
  • فرآیندهای فرزند می‌توانند حتی بدون detached نیز پس از خروج والد ادامه دهند.

3. مدیریت انتظار والد


  • به‌طور پیش‌فرض، والد منتظر پایان فرآیند فرزند detached می‌ماند.
  • برای جلوگیری از این رفتار، باید از subprocess.unref() استفاده کرد.
  • این کار باعث می‌شود حلقهٔ رویداد والد مستقل از فرزند خاتمه یابد.

4. پیکربندی stdio


  • برای اجرای فرآیند طولانی‌مدت در پس‌زمینه، stdio نباید به والد متصل باشد.
  • اگر stdio والد به ارث برسد، فرزند همچنان به ترمینال کنترل‌کننده متصل خواهد بود.

5. مثال‌ها


اجرای فرآیند طولانی‌مدت با detached و نادیده گرفتن stdio:

const { spawn } = require('node:child_process');
const subprocess = spawn(process.argv[0], ['child_program.js'], {
  detached: true,
  stdio: 'ignore',
});
subprocess.unref();

اجرای فرآیند با خروجی در فایل:

const { openSync } = require('node:fs');
const { spawn } = require('node:child_process');
const out = openSync('./out.log', 'a');
const err = openSync('./out.log', 'a');

const subprocess = spawn('prg', [], {
  detached: true,
  stdio: [ 'ignore', out, err ],
});
subprocess.unref();

نتیجه‌گیری


گزینهٔ detached در ماژول child_process ابزاری قدرتمند برای اجرای فرآیندهای مستقل و طولانی‌مدت است. با استفادهٔ صحیح از unref() و پیکربندی مناسب stdio، می‌توان فرآیندهایی ایجاد کرد که پس از خروج والد همچنان در پس‌زمینه اجرا شوند.

1. گزینهٔ stdio


گزینهٔ stdio نحوهٔ اتصال جریان‌های ورودی/خروجی بین والد و فرزند را تعیین می‌کند:

  • 'pipe': ایجاد pipe بین والد و فرزند (پیش‌فرض).
  • 'overlapped': مشابه pipe اما با پرچم FILE_FLAG_OVERLAPPED (ویژهٔ ویندوز).
  • 'ignore': اتصال fdها به /dev/null و نادیده گرفتن آن‌ها.
  • 'inherit': استفاده از جریان‌های والد (معادل process.stdin, process.stdout, process.stderr).
  • 'ipc': ایجاد کانال IPC برای ارسال پیام بین والد و فرزند.
  • <Stream>: اشتراک‌گذاری یک stream خواندنی/نوشتنی با فرزند.
  • Positive integer: اشتراک‌گذاری یک fd باز در والد با فرزند.
  • null/undefined: استفاده از مقدار پیش‌فرض (pipe برای fdهای 0,1,2 و ignore برای بقیه).
const { spawn } = require('node:child_process');
const process = require('node:process');

// Child inherits parent's stdio
spawn('prg', [], { stdio: 'inherit' });

// Child shares only stderr
spawn('prg', [], { stdio: ['pipe', 'pipe', process.stderr] });

// Extra fd=4 for custom interaction
spawn('prg', [], { stdio: ['pipe', null, null, null, 'pipe'] });

2. ارتباط IPC


  • اگر کانال IPC ایجاد شود و فرزند یک Node.js instance باشد، متدهای process.send() و process.disconnect() فعال می‌شوند.
  • کانال IPC تا زمانی که فرزند handler رویداد ثبت نکند، unref باقی می‌ماند و اجازه می‌دهد فرزند به‌طور طبیعی خاتمه یابد.

3. فرآیندهای همگام


  • spawnSync(), execSync(), execFileSync() فرآیند را به‌صورت همگام اجرا می‌کنند.
  • این متدها حلقهٔ رویداد را بلوکه می‌کنند و برای اسکریپت‌های ساده یا بارگذاری پیکربندی مناسب هستند.

4. execFileSync()


  • مشابه execFile() اما همگام.
  • تا پایان اجرای فرآیند فرزند بازگشت نمی‌کند.
  • در صورت timeout یا exit code غیر صفر، خطا پرتاب می‌شود.
const { execFileSync } = require('node:child_process');

try {
  const stdout = execFileSync('my-script.sh', ['my-arg'], {
    stdio: 'pipe',
    encoding: 'utf8',
  });
  console.log(stdout);
} catch (err) {
  const { stdout, stderr } = err;
  console.error({ stdout, stderr });
}

نتیجه‌گیری


گزینهٔ stdio انعطاف‌پذیری بالایی در مدیریت ارتباط بین والد و فرزند فراهم می‌کند، از pipeهای ساده تا کانال‌های IPC. همچنین متد execFileSync() اجرای همگام فرآیندها را ممکن می‌سازد و برای سناریوهایی که نیاز به بلوکه‌سازی دارند بسیار کاربردی است.

1. execSync()


  • اجرای یک دستور درون shell به‌صورت همگام.
  • تا پایان اجرای فرآیند فرزند بازگشت نمی‌کند.
  • در صورت timeout یا exit code غیر صفر، خطا پرتاب می‌شود.
  • خروجی فرآیند به‌صورت Buffer یا string بازگردانده می‌شود.
const { execSync } = require('node:child_process');
try {
  const output = execSync('ls -lh /usr', { encoding: 'utf8' });
  console.log(output);
} catch (err) {
  console.error('Error:', err);
}

2. spawnSync()


  • مشابه spawn() اما همگام.
  • خروجی شامل pid، stdout، stderr، status و signal است.
  • در صورت timeout یا خطا، یک شیء خطا بازگردانده می‌شود.
const { spawnSync } = require('node:child_process');
const result = spawnSync('ls', ['-lh', '/usr'], { encoding: 'utf8' });

if (result.error) {
  console.error('Error:', result.error);
} else {
  console.log('stdout:', result.stdout);
  console.log('stderr:', result.stderr);
  console.log('status:', result.status);
}

3. Class: ChildProcess


  • نمایندهٔ یک فرآیند فرزند است.
  • از EventEmitter ارث‌بری می‌کند.
  • با متدهای spawn(), exec(), execFile(), fork() ایجاد می‌شود.

رویدادهای مهم:

  • 'exit': پس از پایان فرآیند فرزند.
  • 'close': پس از بسته شدن جریان‌های stdio.
  • 'error': هنگام خطا در ایجاد یا اجرای فرآیند.
  • 'disconnect': پس از قطع ارتباط IPC.
const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process closed with code ${code}`);
});

ls.on('exit', (code) => {
  console.log(`child process exited with code ${code}`);
});

ls.on('error', (err) => {
  console.error('Failed to start subprocess:', err);
});

نتیجه‌گیری


متدهای execSync() و spawnSync() ابزارهای قدرتمندی برای اجرای فرآیندهای فرزند به‌صورت همگام هستند و برای اسکریپت‌های ساده یا بارگذاری پیکربندی در ابتدای برنامه مناسب‌اند. کلاس ChildProcess نیز امکان مدیریت رویدادها و ارتباط با فرآیندهای فرزند را فراهم می‌کند و پایه‌ای برای ساخت برنامه‌های مقیاس‌پذیر و انعطاف‌پذیر محسوب می‌شود.

1. Event: 'message'


  • زمانی فعال می‌شود که فرآیند فرزند با process.send() پیامی ارسال کند.
  • پیام پس از serialization و parsing ممکن است با مقدار اصلی تفاوت داشته باشد.
  • با گزینهٔ serialization: 'advanced' می‌توان داده‌هایی فراتر از JSON ارسال کرد.

2. Event: 'spawn'


  • پس از ایجاد موفقیت‌آمیز فرآیند فرزند فعال می‌شود.
  • اگر فرآیند ایجاد نشود، رویداد 'error' به‌جای آن فعال خواهد شد.
  • این رویداد قبل از دریافت داده از stdout یا stderr رخ می‌دهد.

3. IPC Channel


  • subprocess.channel: مرجع کانال IPC بین والد و فرزند.
  • channel.ref(): نگه‌داشتن حلقهٔ رویداد والد فعال.
  • channel.unref(): اجازهٔ خاتمهٔ والد حتی با کانال باز.

4. Connection Management


  • subprocess.connected: نشان می‌دهد آیا ارتباط IPC هنوز برقرار است.
  • subprocess.disconnect(): بستن کانال IPC و خاتمهٔ ارتباط.

5. Process Control


  • subprocess.exitCode: کد خروج فرآیند فرزند (یا null اگر هنوز فعال باشد).
  • subprocess.kill([signal]): ارسال سیگنال به فرآیند فرزند (پیش‌فرض SIGTERM).
  • subprocess.killed: نشان می‌دهد آیا سیگنال با موفقیت ارسال شده است.
  • subprocess.pid: شناسهٔ فرآیند فرزند.
const { spawn } = require('node:child_process');
const grep = spawn('grep', ['ssh']);

console.log(`Spawned child pid: ${grep.pid}`);
grep.kill('SIGHUP');

6. Reference Management


  • subprocess.unref(): حذف وابستگی از حلقهٔ رویداد والد.
  • subprocess.ref(): بازگرداندن وابستگی و مجبور کردن والد به انتظار برای فرزند.
const { spawn } = require('node:child_process');
const subprocess = spawn(process.argv[0], ['child_program.js'], {
  detached: true,
  stdio: 'ignore',
});

subprocess.unref();
subprocess.ref();

نتیجه‌گیری


کلاس ChildProcess در Node.js مجموعه‌ای از رویدادها و ویژگی‌ها برای مدیریت فرآیندهای فرزند ارائه می‌دهد. با استفاده از رویدادهایی مانند 'message' و 'spawn' و متدهایی مانند kill() و disconnect()، توسعه‌دهندگان می‌توانند کنترل کامل بر فرآیندهای فرزند داشته باشند و ارتباطات IPC را به‌صورت ایمن مدیریت کنند.

1. subprocess.send()


  • ارسال پیام از والد به فرزند از طریق IPC.
  • پیام‌ها پس از serialization و parsing منتقل می‌شوند.
  • در صورت استفاده از serialization: 'advanced' می‌توان داده‌هایی فراتر از JSON ارسال کرد.
  • بازگشت false نشان‌دهندهٔ بسته بودن کانال یا پر بودن صف پیام‌هاست.
// Parent script
const { fork } = require('node:child_process');
const forked = fork(__dirname + '/sub.js');

forked.on('message', (msg) => {
  console.log('PARENT got message:', msg);
});

forked.send({ hello: 'world' });

// Child script (sub.js)
process.on('message', (msg) => {
  console.log('CHILD got message:', msg);
});
process.send({ foo: 'bar', baz: NaN });

2. ارسال server یا socket


  • با استفاده از sendHandle می‌توان یک TCP server یا socket را به فرزند منتقل کرد.
  • فرزند می‌تواند اتصال‌ها را مدیریت کند.
// Parent
const { fork } = require('node:child_process');
const { createServer } = require('node:net');
const child = fork('subprocess.js');

const server = createServer();
server.listen(1337, () => {
  child.send('server', server);
});

// Child
process.on('message', (m, server) => {
  if (m === 'server') {
    server.on('connection', (socket) => {
      socket.end('handled by child');
    });
  }
});

3. ویژگی‌های مرتبط


  • subprocess.signalCode: سیگنال دریافتی توسط فرزند (یا null).
  • subprocess.spawnargs: آرگومان‌های کامل اجرای فرآیند.
  • subprocess.spawnfile: نام فایل اجرایی فرآیند فرزند.

4. نکات مهم


  • پیام‌های با پیشوند NODE_ برای استفاده داخلی Node.js رزرو شده‌اند.
  • ارسال IPC sockets در ویندوز پشتیبانی نمی‌شود.
  • callback اختیاری پس از ارسال پیام فراخوانی می‌شود.

نتیجه‌گیری


متد subprocess.send() و ویژگی‌های مرتبط در کلاس ChildProcess ابزارهای قدرتمندی برای ارتباط بین فرآیند والد و فرزند در Node.js هستند. با استفاده از این قابلیت‌ها می‌توان پیام‌ها، سرورها و سوکت‌ها را به اشتراک گذاشت و فرآیندهای توزیع‌شده و مقیاس‌پذیر ایجاد کرد.

1. subprocess.stderr


  • یک Readable Stream که نمایندهٔ خروجی خطای فرآیند فرزند است.
  • اگر stdio[2] به چیزی غیر از 'pipe' تنظیم شده باشد، مقدار آن null خواهد بود.
  • معادل subprocess.stdio[2].

2. subprocess.stdin


  • یک Writable Stream که ورودی فرآیند فرزند را نشان می‌دهد.
  • اگر stdio[0] به چیزی غیر از 'pipe' تنظیم شده باشد، مقدار آن null خواهد بود.
  • معادل subprocess.stdio[0].

3. subprocess.stdout


  • یک Readable Stream که خروجی استاندارد فرآیند فرزند را نشان می‌دهد.
  • اگر stdio[1] به چیزی غیر از 'pipe' تنظیم شده باشد، مقدار آن null خواهد بود.
  • معادل subprocess.stdio[1].
const { spawn } = require('node:child_process');
const subprocess = spawn('ls');

subprocess.stdout.on('data', (data) => {
  console.log(`Received chunk ${data}`);
});

4. subprocess.stdio


  • یک آرایهٔ پراکنده از pipeها که با گزینهٔ stdio هنگام spawn() تعیین می‌شود.
  • مقادیر 0، 1، و 2 به‌ترتیب معادل stdin، stdout، و stderr هستند.
const subprocess = child_process.spawn('ls', {
  stdio: [
    0,        // Use parent's stdin
    'pipe',   // Pipe stdout
    fs.openSync('err.out', 'w'), // Redirect stderr to file
  ],
});

5. subprocess.unref()


  • به والد اجازه می‌دهد بدون انتظار برای پایان فرزند خاتمه یابد.
  • حلقهٔ رویداد والد وابستگی به فرزند را از دست می‌دهد.
const subprocess = spawn(process.argv[0], ['child_program.js'], {
  detached: true,
  stdio: 'ignore',
});
subprocess.unref();

6. maxBuffer و Unicode


  • حداکثر تعداد بایت مجاز در stdout یا stderr.
  • اگر خروجی بیش از این مقدار باشد، فرآیند فرزند خاتمه می‌یابد.
  • تأثیرگذار بر خروجی‌هایی با کاراکترهای چندبایتی مانند UTF-8.

7. Advanced Serialization


  • افزوده شده در نسخه‌های v13.2.0 و v12.16.0.
  • بر اساس الگوریتم HTML structured clone.
  • پشتیبانی از انواع دادهٔ پیشرفته مانند BigInt، Map، Set، ArrayBuffer، Buffer، Error، RegExp.
  • نیازمند فعال‌سازی با گزینهٔ serialization: 'advanced'.

نتیجه‌گیری


ویژگی‌های stdin, stdout, stderr و آرایهٔ stdio ابزارهای اصلی برای مدیریت ارتباط بین والد و فرزند در Node.js هستند. با استفاده از unref()، maxBuffer و قابلیت serialization پیشرفته، توسعه‌دهندگان می‌توانند کنترل کامل بر رفتار فرآیندهای فرزند داشته باشند و برنامه‌های مقیاس‌پذیر و انعطاف‌پذیر ایجاد کنند.

نوشته و پژوهش شده توسط دکتر شاهین صیامی