Node.jsからプロセス実行:child_processのガイド
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'
- ストリームアクセス:
stdin
、stdout
、stderr
プロパティ - プロセス制御:
kill()
、disconnect()
メソッド - プロセス情報:
pid
、connected
、exitCode
プロパティ
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()
withshell: true
execFile()
withshell: 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パターンを活用することを忘れないでください。