メインコンテンツまでスキップ

「Child Process」タグの記事が1件件あります

Node.js子プロセス管理と外部コマンド実行

全てのタグを見る

Node.jsからプロセス実行:child_processのガイド

· 約14分
Mikyan
白い柴犬

Node.jsで外部プロセスを扱う際は、具体的なニーズに基づいて適切な手法を選択してください:

  • exec()を使用 - 短時間実行で小さな出力のコマンドをシェル環境で実行したい場合
  • spawn()を使用 - 長時間実行のプロセス、大きな出力、stdinで入力コントロール、またはリアルタイムでのデータストリーミングが必要な場合
  • execFile()を使用 - シェルを介さずに特定のファイルを実行してセキュリティを向上させたい場合
  • fork()を使用 - Node.jsモジュールを別プロセスでIPC通信とともに実行する場合
  • ユーザー入力の必須サニタイズ - シェルモードでコマンド実行時はインジェクション攻撃を防ぐため
  • util.promisify()の活用を検討 - コールバックベースのメソッドをPromiseに変換してasync/await構文をよりクリーンに使用

プロセスと子プロセスの紹介

プロセスとは?

プロセスとは、実行中のプログラムのインスタンスです。システム上のすべてのアプリケーションは一つ以上のプロセスとして動作し、それぞれが独自のメモリ空間とシステムリソースを持ちます。Node.jsでは、アプリケーションは固有のプロセスID(PID)を持つ単一のプロセスとして動作します。

子プロセスの理解

子プロセスとは、既存のプロセス(親プロセス)によって作成された新しいプロセスです。Node.jsの文脈では:

  • メインのNode.jsアプリケーションが親プロセス
  • 実行する外部プログラムが子プロセスになる
  • 子プロセスは独自のPIDとメモリ空間を持つ
  • 標準I/Oストリーム(stdin、stdout、stderr)を通じて通信が行われる

なぜ子プロセスを使うのか?

子プロセスを使用することで以下が可能になります:

  • システムコマンドや外部プログラムの実行
  • メインイベントループをブロックせずにCPU集約的なタスクを実行
  • 他の言語で書かれた既存のツールやユーティリティの活用
  • 複数のコアにわたってアプリケーションをスケール

Node.js child_process API概要

Node.jsは、子プロセスを作成するための4つの主要メソッドを持つ組み込みchild_processモジュールを提供します:

const { spawn, exec, execFile, fork } = require('child_process');

ChildProcessクラス

すべての子プロセスメソッドはChildProcessクラスのインスタンスを返します。このクラスはEventEmitterを拡張し、以下を提供します:

  • イベントハンドリング: 'exit'、'close'、'error'、'disconnect'
  • ストリームアクセス: stdinstdoutstderrプロパティ
  • プロセス制御: kill()disconnect()メソッド
  • プロセス情報: pidconnectedexitCodeプロパティ
const child = spawn('ls', ['-la']);

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

child.on('exit', (code) => {
console.log(`プロセスが終了しました。終了コード: ${code}`);
});

共通オプション

ほとんどの子プロセスメソッドは、共通プロパティを持つオプションオブジェクトを受け入れます:

const options = {
cwd: '/path/to/working/directory', // 作業ディレクトリ
env: { ...process.env, NODE_ENV: 'production' }, // 環境変数
shell: true, // シェルで実行
timeout: 5000, // 最大実行時間
maxBuffer: 1024 * 1024, // stdout/stderrの最大バッファサイズ
stdio: 'inherit' // stdin/stdout/stderrの処理方法
};

メソッド間の類似点

戻り値の型(非同期)

すべての非同期メソッドはChildProcessインスタンスを返します:

const child1 = spawn('echo', ['hello']);
const child2 = exec('echo hello');
const child3 = execFile('echo', ['hello']);
const child4 = fork('./child-script.js');

同期バリアント

ほとんどのメソッドには実行をブロックする同期版があります:

  • spawnSync()
  • execSync()
  • execFileSync()

同期メソッドは結果オブジェクトを返します:

{
pid: 12345,
output: [], // stdioの結果配列
stdout: '', // 標準出力(BufferまたはString)
stderr: '', // 標準エラー(BufferまたはString)
status: 0, // 終了コード(シグナルで終了した場合はnull)
signal: null, // プロセスを終了させたシグナル
error: undefined // プロセスが失敗した場合のErrorオブジェクト
}

イベント駆動アーキテクチャ

すべての子プロセスは同じ主要イベントを発火します:

child.on('spawn', () => {});     // プロセス開始
child.on('exit', (code, signal) => {}); // プロセス終了
child.on('error', (error) => {}); // エラー発生
child.on('close', (code, signal) => {}); // すべてのstdioストリームが閉じられた

メソッド間の違い

spawn()

目的: ストリーミングI/Oを使用した低レベルプロセス作成

特徴:

  • デフォルトではシェルを使用しない
  • 即座にChildProcessを返す
  • リアルタイムでデータをストリーミング
  • 自動バッファリングなし
  • 最もメモリ効率が良い
const child = spawn('grep', ['pattern'], {
stdio: ['pipe', 'pipe', 'pipe']
});

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

最適な用途: 長時間実行プロセス、大きな出力、リアルタイムデータ処理

exec()

目的: バッファ出力を持つシェルでのコマンド実行

特徴:

  • 常にシェルを使用
  • 出力全体をメモリにバッファ
  • 完全なstdout/stderrを持つコールバックを提供
  • maxBuffer制限の対象
  • シェル機能(パイプ、リダイレクトなど)をサポート
exec('ls -la | grep .js', (error, stdout, stderr) => {
if (error) {
console.error(`エラー: ${error}`);
return;
}
console.log(`ファイル: ${stdout}`);
});

最適な用途: シンプルなシェルコマンド、小さな出力、一回限りの操作

注意: exec()も他のメソッド同様ChildProcessインスタンスを返し、stdin、stdout、stderrのストリームデータを取得できますが、内部的なBuffer処理により、大きな出力ではパフォーマンスに影響を与える可能性があります。

execFile()

目的: シェルを介さない直接的なファイル実行

特徴:

  • デフォルトではシェルを使用しない
  • exec()よりも安全
  • exec()と同様に出力をバッファ
  • 直接的なファイル実行
  • shell: trueオプションでシェルを有効化可能
execFile('node', ['--version'], (error, stdout, stderr) => {
if (error) {
console.error(`エラー: ${error}`);
return;
}
console.log(`Nodeバージョン: ${stdout}`);
});

最適な用途: プログラムの安全な実行、シェルインジェクションの回避

fork()

目的: IPC通信を持つ新しいNode.jsプロセスの作成

特徴:

  • Node.jsモジュール専用
  • IPC(プロセス間通信)チャンネルを確立
  • Node.js環境を継承
  • 組み込みメッセージパッシング
  • 独立したV8インスタンス
// parent.js
const child = fork('./worker.js');
child.send({ task: 'process-data', data: largeDataSet });
child.on('message', (result) => {
console.log('受信:', result);
});

// worker.js
process.on('message', (msg) => {
if (msg.task === 'process-data') {
const result = processData(msg.data);
process.send({ result });
}
});

最適な用途: CPU集約的なタスク、Node.jsモジュール実行、ワーカープロセス

実践的な使用例

タイムアウト処理

長時間実行されるプロセスには適切なタイムアウトを設定しましょう:

const { spawn } = require('child_process');

function runWithTimeout(command, args, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
const child = spawn(command, args);
let output = '';
let errorOutput = '';

// タイムアウトタイマー設定
const timer = setTimeout(() => {
child.kill('SIGTERM');
reject(new Error(`プロセスがタイムアウトしました: ${timeoutMs}ms`));
}, timeoutMs);

child.stdout.on('data', (data) => {
output += data.toString();
});

child.stderr.on('data', (data) => {
errorOutput += data.toString();
});

child.on('close', (code) => {
clearTimeout(timer);
if (code === 0) {
resolve(output);
} else {
reject(new Error(`プロセスが異常終了: コード ${code}, エラー: ${errorOutput}`));
}
});

child.on('error', (error) => {
clearTimeout(timer);
reject(error);
});
});
}

// 使用例
runWithTimeout('ping', ['-c', '3', 'google.com'], 10000)
.then(output => console.log('結果:', output))
.catch(error => console.error('エラー:', error.message));

AbortControllerを使用したプロセス中断

Node.js 15+では、AbortControllerを使用してプロセスを中断できます:

const { spawn } = require('child_process');
const { AbortController } = require('abort-controller');

async function runWithAbort(command, args, signal) {
return new Promise((resolve, reject) => {
const child = spawn(command, args);
let output = '';

// AbortSignalが発火したときの処理
if (signal) {
signal.addEventListener('abort', () => {
child.kill('SIGTERM');
reject(new Error('プロセスが中断されました'));
});
}

child.stdout.on('data', (data) => {
output += data.toString();
});

child.on('close', (code) => {
if (code === 0) {
resolve(output);
} else {
reject(new Error(`プロセスが失敗: ${code}`));
}
});

child.on('error', reject);
});
}

// 使用例
const controller = new AbortController();
const { signal } = controller;

// 5秒後に中断
setTimeout(() => controller.abort(), 5000);

runWithAbort('sleep', ['10'], signal)
.then(output => console.log('完了:', output))
.catch(error => console.error('エラー:', error.message));

対話型プロセスの処理

stdinを使用してプロセスと対話する例:

const { spawn } = require('child_process');

function interactiveProcess() {
const child = spawn('node', ['-i'], {
stdio: ['pipe', 'pipe', 'pipe']
});

// コマンドを送信
child.stdin.write('console.log("Hello from child process");\n');
child.stdin.write('process.version;\n');
child.stdin.write('.exit\n');

child.stdout.on('data', (data) => {
console.log('出力:', data.toString());
});

child.stderr.on('data', (data) => {
console.error('エラー:', data.toString());
});

child.on('close', (code) => {
console.log(`対話型プロセスが終了: ${code}`);
});
}

interactiveProcess();

環境変数とワーキングディレクトリの設定

const { spawn } = require('child_process');
const path = require('path');

function runWithCustomEnv() {
const child = spawn('node', ['-e', 'console.log(process.env.CUSTOM_VAR, process.cwd())'], {
cwd: path.join(__dirname, 'subdir'), // 作業ディレクトリを変更
env: {
...process.env,
CUSTOM_VAR: 'カスタム値',
NODE_ENV: 'development'
}
});

child.stdout.on('data', (data) => {
console.log('出力:', data.toString());
});

child.on('error', (error) => {
console.error('実行エラー:', error);
});
}

runWithCustomEnv();

大量データの効率的な処理

大きなファイルやストリームを処理する際のパフォーマンス最適化:

const { spawn } = require('child_process');
const fs = require('fs');

function processLargeFile(inputFile, outputFile) {
return new Promise((resolve, reject) => {
const child = spawn('gzip', ['-c'], {
stdio: ['pipe', 'pipe', 'pipe']
});

const readStream = fs.createReadStream(inputFile);
const writeStream = fs.createWriteStream(outputFile);

// ストリームをパイプで接続
readStream.pipe(child.stdin);
child.stdout.pipe(writeStream);

// エラーハンドリング
readStream.on('error', reject);
writeStream.on('error', reject);
child.stderr.on('data', (data) => {
console.error('gzipエラー:', data.toString());
});

child.on('close', (code) => {
if (code === 0) {
resolve(`ファイル圧縮完了: ${outputFile}`);
} else {
reject(new Error(`圧縮失敗: ${code}`));
}
});
});
}

// 使用例
processLargeFile('large-file.txt', 'large-file.txt.gz')
.then(message => console.log(message))
.catch(error => console.error(error));

プロセスプールパターン

複数の同時実行プロセスを管理:

const { spawn } = require('child_process');

class ProcessPool {
constructor(maxConcurrency = 3) {
this.maxConcurrency = maxConcurrency;
this.running = new Set();
this.queue = [];
}

async execute(command, args) {
return new Promise((resolve, reject) => {
const task = { command, args, resolve, reject };

if (this.running.size < this.maxConcurrency) {
this.runTask(task);
} else {
this.queue.push(task);
}
});
}

runTask(task) {
const { command, args, resolve, reject } = task;
const child = spawn(command, args);

this.running.add(child);
let output = '';
let errorOutput = '';

child.stdout.on('data', (data) => {
output += data.toString();
});

child.stderr.on('data', (data) => {
errorOutput += data.toString();
});

child.on('close', (code) => {
this.running.delete(child);

if (code === 0) {
resolve(output);
} else {
reject(new Error(`プロセス失敗: ${errorOutput}`));
}

// キューから次のタスクを実行
if (this.queue.length > 0) {
const nextTask = this.queue.shift();
this.runTask(nextTask);
}
});

child.on('error', (error) => {
this.running.delete(child);
reject(error);

if (this.queue.length > 0) {
const nextTask = this.queue.shift();
this.runTask(nextTask);
}
});
}
}

// 使用例
const pool = new ProcessPool(2);

const tasks = [
pool.execute('echo', ['タスク1']),
pool.execute('echo', ['タスク2']),
pool.execute('echo', ['タスク3']),
pool.execute('echo', ['タスク4'])
];

Promise.all(tasks)
.then(results => {
console.log('すべてのタスク完了:', results);
})
.catch(error => {
console.error('タスクエラー:', error);
});

セキュリティ上の考慮事項

シェルインジェクション防止

シェルモードを使用する際は、常にユーザー入力をサニタイズしてください:

// ❌ 危険 - これは絶対にやってはいけません
const userInput = req.body.filename;
exec(`cat ${userInput}`, callback); // インジェクション攻撃に脆弱

// ✅ 安全 - spawnで配列引数を使用
spawn('cat', [userInput], callback);

// ✅ 安全 - 入力を検証・サニタイズ
const safeFilename = path.basename(userInput).replace(/[^a-zA-Z0-9.-]/g, '');
exec(`cat ${safeFilename}`, callback);

シェルインジェクションに脆弱なメソッド

  • exec() (常にシェルを使用)
  • spawn() with shell: true
  • execFile() with shell: true

より安全な代替案

  • シェルモードなしでspawn()またはexecFile()を使用
  • すべてのユーザー入力を検証・サニタイズ
  • 許可される値のホワイトリストを使用
  • 適切なエスケープのためにshell-escapeなどのライブラリを検討

モダンなAsync/Await使用法

コールバックベースのメソッドをPromiseに変換:

const { promisify } = require('util');
const execAsync = promisify(exec);

async function getNodeVersion() {
try {
const { stdout } = await execAsync('node --version');
return stdout.trim();
} catch (error) {
console.error('Nodeバージョンの取得に失敗:', error);
throw error;
}
}

最終結論

Node.jsのchild_processモジュールは、外部プロセスを実行するための強力なツールを提供します。ニーズに基づいて適切なメソッドを選択してください:

  • パフォーマンス重視のアプリケーション: ストリーミング機能と低メモリフットプリントのためspawn()を使用
  • シンプルなシェルコマンド: 小さな出力での便利さのためexec()を使用
  • セキュリティ意識の高いアプリケーション: シェルモードなしでexecFile()またはspawn()を選択
  • Node.jsスケーリング: 並列化可能なCPU集約的タスクにはfork()を使用

特にユーザー入力を扱う際は常にセキュリティへの影響を考慮し、よりクリーンで保守しやすいコードのためにモダンなasync/awaitパターンを活用することを忘れないでください。