Node JS: les child process

Objectif JSNAD : Maîtriser la création et la gestion de processus enfants est une compétence clé pour l’examen. Cet article couvre toutes les subtilités du module child_process.

1. Fondamentaux des Child Processes

Node.js utilise le modèle d’I/O non bloquant mais parfois on doit :

– Exécuter des binaires système (ex: ffmpeg)
– Paralléliser des tâches CPU-intensive
– Créer des architectures microservices

Architecture Typique :


Processus Parent (Node.js)
        |
        ├── Child Process 1 (ls -la)
        ├── Child Process 2 (Python script)
        └── Child Process 3 (Fork Node.js)

2. Méthodes Détaillées avec Exemples Avancés

2.1 spawn() – La méthode bas niveau

Contrôle fin des flux d’entrée/sortie :

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

// Exécution avec options avancées
const child = spawn('grep', ['ERROR'], {
    stdio: [
        'pipe', // stdin
        'pipe', // stdout
        process.stderr // rediriger stderr vers le parent
    ],
    detached: true,
    cwd: '/var/log'
});

// Écriture dans stdin
child.stdin.write('Log line 1\n');
child.stdin.write('Log line 2 ERROR\n');
child.stdin.end(); // Fermeture nécessaire

// Lecture ligne par ligne
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
    console.log('Résultat filtré:', chunk.trim());
});

// Gestion des erreurs étendue
child.on('error', (err) => {
    console.error('Erreur du processus:', err);
});

child.on('exit', (code, signal) => {
    if (code === 0) {
        console.log('Succès');
    } else {
        console.log(`Échec avec code ${code}`);
    }
});

2.2 exec() – Pour l’interaction avec le shell

Avec gestion de timeout et buffer limit :

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

exec(
    'find / -name "*.log"',
    {
        timeout: 5000, // Annule après 5s
        maxBuffer: 1024 * 1024, // 1MB max
        encoding: 'utf8'
    },
    (error, stdout, stderr) => {
        if (error) {
            if (error.signal === 'SIGTERM') {
                console.log('Processus annulé par timeout');
            }
            return;
        }
        console.log('Fichiers logs:', stdout.split('\n'));
    }
);

2.3 execFile() – Exécution sécurisée

Évite l’injection de commandes :

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

// Sécurisé : les arguments sont passés dans un tableau
execFile(
    'convert',
    ['input.jpg', '-resize', '50%', 'output.jpg'],
    (error, stdout, stderr) => {
        // Gestion des erreurs...
    }
);

2.4 fork() – Communication avancée

Messagerie bidirectionnelle avec sérialisation JSON :

// parent.js
const { fork } = require('child_process');
const child = fork('./compute.js', { silent: true });

child.on('message', (message) => {
    console.log('Résultat:', message.result);
});

child.send({ 
    task: 'fibonacci',
    data: { n: 40 }
});

// compute.js
process.on('message', (message) => {
    if (message.task === 'fibonacci') {
        const result = fib(message.data.n);
        process.send({ result });
    }
});

function fib(n) { /* ... */ }

3. Quand utiliser spawn(), exec() ou fork() ?

Clé de réussite JSNAD : Savoir choisir la bonne méthode en fonction du cas d’usage est fondamental pour l’examen.

3.1 child_process.spawn() : Le spécialiste des flux

Utilisation idéale quand :
– 🗂 Traiter des données en streaming (vidéo, logs, gros fichiers)
– ⏳ Gérer des processus de longue durée
– 🎮 Contrôler manuellement les entrées/sorties

Exemple typique :

// Conversion vidéo avec flux progressif
const ffmpeg = spawn('ffmpeg', ['-i', 'input.mp4', 'output.avi']);

ffmpeg.stderr.on('data', (data) => {
    // Affichage progression en temps réel
    console.log('Progression:', data.toString());
});

3.2 child_process.exec() : Le rapide pour commandes shell

Utilisation idéale quand :
– ⚡ Exécuter des commandes courtes
– 📋 Utiliser des fonctionnalités shell (pipes, redirections)
– 🔢 Récupérer un résultat unique

Exemple typique :

// Comptage rapide de fichiers
exec('ls -l | wc -l', (err, stdout) => {
    console.log(`Nombre d'éléments: ${stdout}`);
});

Attention : Ne pas utiliser pour des sorties volumineuses (risque de dépassement de buffer) !

3.3 child_process.fork() : La communication Node.js

Utilisation idéale quand :
– 🧠 Paralléliser des calculs lourds
– 📡 Communiquer entre processus
– 🔄 Créer des architectures en cluster

Exemple typique :

// parent.js
const worker = fork('image-processor.js');

// Envoi de tâche de traitement d'image
worker.send({ image: buffer, options: { width: 800 } });

// Réception résultat
worker.on('message', (processedImage) => {
    // Affichage résultat
});

3.4 Comparaison Technique

Méthode Détails
spawn Modèle d’exécution: Flux événementiel
Surcoût: Faible
Transfert de données: Chunks bruts
Cas optimal: Flux vidéo
Shell: Non*
Performance: Haute
Streaming: Oui
Sécurité: Élevée
Use Case: Flux volumineux
exec Modèle d’exécution: Buffer unique
Surcoût: Élevé (shell)
Transfert de données: String
Cas optimal: Commande shell simple
Shell: Oui
Performance: Moyenne
Streaming: Non
Sécurité: Faible
Use Case: Commandes courtes
fork Modèle d’exécution: IPC Node.js
Surcoût: Moyen
Transfert de données: Objets sérialisés
Cas optimal: Calcul matriciel
Shell: Non
Performance: Variable
Streaming: Oui
Sécurité: Élevée
Use Case: Cluster Node.js

* Peut utiliser un shell avec l’option { shell: true }

4. Gestion des Erreurs Avancée

Codes de Sortie Unix

child.on('exit', (code, signal) => {
    if (code === 0) {
        console.log('Succès');
    } else if (code === 1) {
        console.error('Erreur générale');
    } else if (code === 2) {
        console.error('Abus de shell');
    }
    if (signal === 'SIGKILL') {
        console.log('Processus tué');
    }
});

Pattern de Gestion Centralisée

function runCommand(command, args) {
    const child = spawn(command, args);
    
    const promise = new Promise((resolve, reject) => {
        let stdout = '';
        let stderr = '';
        
        child.stdout.on('data', (data) => stdout += data);
        child.stderr.on('data', (data) => stderr += data);
        
        child.on('close', (code) => {
            if (code === 0) {
                resolve(stdout);
            } else {
                const error = new Error(`Command failed: ${stderr}`);
                error.code = code;
                reject(error);
            }
        });
    });
    
    promise.child = child;
    return promise;
}

5. Sécurité Renforcée

Alerte Injection : Jamais utiliser exec avec des entrées utilisateur non nettoyées !
// Dangereux !
exec(`rm ${userInput}`);

// Sécurisé
execFile('rm', [userInput], {
    shell: false // Désactive l'interprétation shell
});

Bonnes Pratiques de Sécurité :

– Utiliser execFile par défaut
– Valider et sanitizer toutes les entrées
– Limiter les permissions avec uid/gid
– Utiliser timeout et maxBuffer

6. Techniques Avancées JSNAD

Redirection de Flux Complexe

// Rediriger stdout vers un fichier
const fs = require('fs');
const { spawn } = require('child_process');

const out = fs.createWriteStream('output.log');

const child = spawn('program', [], {
    stdio: ['ignore', out, 'inherit']
});

Cluster de Processes

const cluster = require('cluster');
const http = require('http');

if (cluster.isMaster) {
    // Fork 4 workers
    for (let i = 0; i < 4; i++) {
        cluster.fork();
    }
} else {
    http.createServer((req, res) => {
        res.end('Hello World\n');
    }).listen(8000);
}

7. Debugging des Child Processes

Options utiles :

// Activer le mode debug
NODE_DEBUG=child_process node app.js

// Obtenir des informations système
const psTree = require('ps-tree');
psTree(process.pid, (err, children) => {
    console.log('Processus enfants:', children);
});

8. Communication entre parent et enfant

En Node.js, la communication entre un processus parent et un processus enfant se fait via des pipes (flux) en utilisant child_process.fork() ou d’autres méthodes du module child_process.

1. Création d’un processus enfant avec fork()

fork() est une méthode spécifique pour exécuter un script Node.js en tant que processus enfant et permet une communication bidirectionnelle entre le parent et l’enfant via process.send() et l’événement message.

Exemple 1 : Communication Parent → Enfant → Parent

Le parent envoie un message à l’enfant, l’enfant le traite et répond.

const { fork } = require("child_process");

// Création du processus enfant
const child = fork("./child.js");

// Envoi d'un message à l'enfant
child.send({ action: "greet", name: "Alice" });

// Réception de la réponse de l'enfant
child.on("message", (message) => {
  console.log("Message du processus enfant :", message);
});
// Écoute des messages envoyés par le parent
process.on("message", (message) => {
  if (message.action === "greet") {
    // Réponse au parent
    process.send({ response: `Bonjour, ${message.name} !` });
  }
});

Résultat attendu dans la console du parent :

Message du processus enfant : { response: 'Bonjour, Alice !' }

2. Utilisation des flux (spawn et exec)

Avec spawn() ou exec(), la communication se fait via stdin, stdout, et stderr.

Exemple 2 : Lire la sortie d’un script enfant

Ici, le parent lance un script enfant et lit son output.

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

const child = spawn("node", ["child.js"]);

child.stdout.on("data", (data) => {
  console.log(`Message du processus enfant : ${data}`);
});

child.stderr.on("data", (data) => {
  console.error(`Erreur du processus enfant : ${data}`);
});
console.log("Bonjour depuis l'enfant !");

Résultat attendu dans la console du parent :

Message du processus enfant : Bonjour depuis l'enfant !

3. Interaction plus complexe avec stdin

L’enfant peut recevoir des données du parent via stdin et répondre via stdout.

Exemple 3 : Envoyer une entrée et recevoir une sortie

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

const child = spawn("node", ["child.js"]);

// Envoi d'une donnée à l'enfant via stdin
child.stdin.write("42\n");

child.stdout.on("data", (data) => {
  console.log(`Réponse de l'enfant : ${data}`);
});
process.stdin.on("data", (data) => {
  const number = parseInt(data.toString().trim(), 10);
  console.log(`Le double de ${number} est ${number * 2}`);
});

Résultat attendu dans la console du parent :

Réponse de l'enfant : Le double de 42 est 84

Résumé

  • fork() : Utilisé pour exécuter un script Node.js et communiquer via process.send().
  • spawn() : Utilisé pour exécuter un processus et communiquer via stdin, stdout, et stderr.
  • exec() : Semblable à spawn(), mais retourne un buffer complet plutôt qu’un flux.

Chaque méthode est utile selon les besoins, fork() étant le plus adapté pour une communication bidirectionnelle entre un parent et un script Node.js.

8. Préparation JSNAD : Checklist

– ✓ Savoir choisir entre spawn/exec/fork
– ✓ Maîtriser les options stdio
– ✓ Comprendre les codes de sortie
– ✓ Implémenter un système de logging inter-processus
– ✓ Savoir sécuriser les appels système

Conclusion

La maîtrise des child processes est essentielle pour créer des applications Node.js professionnelles. En combinant les bonnes pratiques de sécurité, les patterns de gestion d’erreur et les techniques avancées présentées ici, vous serez parfaitement préparé pour la certification JSNAD et les projets réels.

Rédigé par

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.