Préambule

Ce travail pratique vous permettra de découvrir les mécanismes essentiels de la gestion des processus système depuis une application Java. Vous apprendrez à lancer des programmes externes, contrôler leurs flux d’entrée/sortie, et gérer leur cycle de vie.

Étape 0 : Code de base et concepts fondamentaux

Contexte technique

En Java, il existe deux façons principales de lancer un processus externe : ProcessBuilder et Runtime.exec(). La classe ProcessBuilder est l’approche moderne et recommandée, car elle offre un contrôle plus fin sur l’environnement d’exécution, une gestion simplifiée des flux d’entrée/sortie et une API fluide pour préparer et lancer un processus. Par exemple, pour exécuter la commande ls -l sur un système Unix :

ProcessBuilder builder = new ProcessBuilder("ls", "-l");
Process process = builder.start();

À l’inverse, Runtime.exec() est une méthode héritée qui fonctionne toujours mais présente plusieurs limitations : difficulté à gérer des arguments contenant des espaces, contrôle limité de l’environnement, et manipulation des flux moins intuitive. Voici l’équivalent avec Runtime.exec() :

Process process = Runtime.getRuntime().exec("ls -l");

Organisation du projet

OS_Nom1_Nom2/
├─ .gitignore                   # fichiers à ignorer (*.class, *.jar, etc.)
├─ README.md                    # informations additionnelles
└─ ProcessManager/
   ├─ .gitignore
   ├─ README.md
   ├─ ProcessController.java    # gestionnaire principal des processus
   └─ Main.java                 # Vos tests et démonstrations

ProcessController.java

import java.io.*;

/**
 * Gestionnaire principal pour le lancement et le contrôle de processus externes.
 * Cette classe encapsule les fonctionnalités de ProcessBuilder en offrant
 * une interface simplifiée pour la gestion des processus.
 */
public class ProcessController {

    public static final int DEFAULT_TIMEOUT_SECONDS = 30;

    // Variables d'instance
    private ProcessBuilder processBuilder;
    private Process currentProcess;

    public ProcessController() {
        this.processBuilder = new ProcessBuilder();
        this.currentProcess = null;
    }

    /**
     * Lance un processus simple avec une commande et ses arguments.
     * Cette méthode constitue le point d'entrée basique pour l'exécution
     * de programmes externes.
     *
     * @param command commande à exécuter (ex: "ls", "python3", "notepad.exe")
     * @param args arguments optionnels de la commande
     * @return le processus lancé
     * @throws IOException si le lancement échoue
     */
    public Process executeSimple(String command, String[] args) throws IOException {
        // TODO Créer un tableau pour stocker la commande complète
        String[] fullCommand = null;

        // TODO Si args est null, fullCommand = tableau avec juste command
        // TODO Sinon, fullCommand = tableau avec command + tous les args

        // TODO Configurer le ProcessBuilder avec fullCommand
        // processBuilder.command(fullCommand);

        // TODO Lancer le processus avec processBuilder.start()
        currentProcess = null;

        System.out.println("Lancement de : " + command);
        return currentProcess;
    }

    /**
     * Lance un processus avec redirection des flux vers des fichiers.
     * Permet de capturer facilement les sorties standard et d'erreur.
     *
     * @param command commande à exécuter
     * @param outputFile fichier pour la sortie standard (null = pas de redirection)
     * @param errorFile fichier pour la sortie d'erreur (null = pas de redirection)  
     * @param args arguments de la commande
     * @return le processus configuré et lancé
     * @throws IOException si la configuration ou le lancement échoue
     */
    public Process executeWithRedirection(String command, File outputFile, 
                                        File errorFile, String[] args) throws IOException {

        // TODO Utiliser executeSimple pour lancer le processus de base
        Process process = null;

        // TODO Si outputFile n'est pas null, configurer la redirection
        // processBuilder.redirectOutput(outputFile);

        // TODO Si errorFile n'est pas null, configurer la redirection d'erreur
        // processBuilder.redirectError(errorFile);

        System.out.println("Redirection configurée - Sortie: " + outputFile + ", Erreur: " + errorFile);

        // TODO Relancer le processus avec les redirections
        currentProcess = null;
        return currentProcess;
    }

    /**
     * Lance un processus interactif permettant l'envoi de données via l'entrée standard
     * et la lecture temps réel des sorties.
     *
     * @param command commande à lancer
     * @param args arguments
     * @return le processus interactif
     * @throws IOException si le lancement échoue
     */
    public Process executeInteractive(String command, String[] args) throws IOException {
        // TODO Utiliser executeSimple pour lancer le processus
        // (Les flux restent accessibles par défaut)

        System.out.println("Mode interactif activé pour : " + command);

        return null;
    }

    /**
     * Attend la fin d'exécution d'un processus avec un timeout optionnel.
     * Retourne le code de sortie.
     *
     * @param process processus à attendre
     * @param timeoutSeconds délai maximum d'attente (0 = pas de timeout)
     * @return code de sortie du processus (-1 si timeout)
     * @throws InterruptedException si l'attente est interrompue
     */
    public int waitForProcess(Process process, int timeoutSeconds) throws InterruptedException {

        if (timeoutSeconds <= 0) {
            // TODO Attendre indéfiniment avec process.waitFor()
            return 0;
        } else {
            // TODO Utiliser process.waitFor(timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)
            // TODO Si le processus se termine dans les temps, retourner process.exitValue()
            // TODO Sinon, appeler process.destroyForcibly() et retourner -1
            return -1;
        }
    }

    /**
     * Envoie des données à l'entrée standard d'un processus interactif.
     */
    public void sendInput(Process process, String input) throws IOException {
        // TODO Obtenir l'OutputStream du processus
        OutputStream outputStream = null;

        if (outputStream != null) {
            // TODO Écrire les données + retour à la ligne
            // TODO Appeler flush() pour forcer l'envoi
        }

        System.out.println("Envoi vers le processus : " + input);
    }

    /**
     * Lit la sortie standard d'un processus de manière non-bloquante.
     */
    public String readOutput(Process process) throws IOException {
        // TODO Obtenir l'InputStream du processus
        InputStream inputStream = null;

        if (inputStream != null) {
            // TODO Vérifier s'il y a des données avec inputStream.available()
            // TODO Si oui, les lire et les retourner comme String
        }

        return "";
    }

    // Getters
    public Process getCurrentProcess() { 
        return currentProcess; 
    }
}

Attention : Les parties marquées TODO constituent le cœur de votre travail. Ne modifiez pas la structure générale des classes, concentrez-vous sur l’implémentation des fonctionnalités demandées.

Étape 1 : Lancement simple de processus

La méthode ProcessBuilder.command() accepte une liste d’arguments où le premier élément est toujours la commande, et les suivants sont ses paramètres. Cette séparation évite les problèmes de parsing des espaces et caractères spéciaux.
Exemple concret :

// Correct
String[] cmd = {"ls", "-la", "/home"};
ProcessBuilder pb = new ProcessBuilder(cmd);

// Aussi correct  
ProcessBuilder pb = new ProcessBuilder("ls", "-la", "/home");

// Incorrect (parsing problématique)
ProcessBuilder pb = new ProcessBuilder("ls -la /home");

Complétez la méthode executeSimple() dans ProcessController :

  1. Créez une List<String> et ajoutez-y la commande puis tous les arguments
  2. Configurez le ProcessBuilder avec processBuilder.command(listComplete)
  3. Lancez le processus avec processBuilder.start()
  4. Stockez le processus dans currentProcess et retournez-le

Test recommandé : Testez avec "echo", "Hello World" sous Linux/Mac ou "cmd", "/c", "echo Hello World" sous Windows.

Étape 2 : Redirection des flux

ProcessBuilder propose plusieurs modes de redirection des flux :

  • redirectOutput(File) : redirige la sortie standard vers un fichier
  • redirectError(File) : redirige la sortie d’erreur vers un fichier
  • redirectErrorStream(true) : fusionne erreur et sortie standard
  • inheritIO() : hérite des flux du processus parent (affichage direct)

Pourquoi rediriger ? Cela permet de :

  • Sauvegarder les résultats d’exécution
  • Séparer les messages d’erreur des données utiles
  • Éviter que les buffers de sortie se remplissent et bloquent le processus

Complétez la méthode executeWithRedirection() :

  1. Construisez la liste complète commande + arguments
  2. Si outputFile n’est pas null : processBuilder.redirectOutput(outputFile)
  3. Si errorFile n’est pas null : processBuilder.redirectError(errorFile)
  4. Lancez le processus et retournez-le

Exemple :

ProcessBuilder pb = new ProcessBuilder("ls", "-la", "/");
pb.redirectOutput(new File("listing.txt"));    // Sortie dans listing.txt
pb.redirectError(new File("errors.txt"));      // Erreurs dans errors.txt
Process process = pb.start();

Étape 3 : Interaction avec des processus

Un processus interactif maintient ses flux d’entrée/sortie ouverts pour permettre un dialogue. Les méthodes clés sont :

  • process.getOutputStream() : pour envoyer des données AU processus
  • process.getInputStream() : pour lire les données DU processus
  • process.getErrorStream() : pour lire les erreurs du processus

Attention à la confusion : L’OutputStream vous permet d’écrire VERS le processus (son entrée), tandis que l’InputStream vous permet de lire DEPUIS le processus (sa sortie). Dit autrement c’est la sortie de VOTRE prgramme.

Complétez les méthodes executeInteractive(), sendInput(), et readOutput() :

Pour executeInteractive() :

  • Ne configurez aucune redirection automatique (pas de redirectOutput())
  • Lancez simplement le processus pour garder les flux accessibles

Pour sendInput() :

  • Obtenez l’OutputStream du processus
  • Écrivez les données + \n
  • Appelez flush() pour l’envoi immédiat

Pour readOutput() :

  • Obtenez l’InputStream du processus
  • Vérifiez available() > 0 pour éviter le blocage
  • Lisez les données disponibles dans un buffer

Test interactif : Lancez python3 ou un autre programme interactif et envoyez-lui des instructions comme print("Hello").

Étape 4 : Synchronisation et terminaison des processus

Chaque processus se termine avec un code de sortie (exit code) :

  • 0 : succès
  • non-zéro : erreur (la valeur indique le type d’erreur)
    La méthode waitFor() bloque jusqu’à la fin du processus, tandis que waitFor(timeout, unit) permet d’éviter l’attente infinie.

Gestion des processus bloqués : Si un timeout survient, il faut impérativement appeler process.destroy() ou process.destroyForcibly() pour libérer les ressources système.

Complétez la méthode waitForProcess() :

  1. Si timeoutSeconds == 0 : utilisez process.waitFor() et retournez le code
  2. Sinon : utilisez process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
  3. Si waitFor() retourne true : le processus s’est terminé, retournez process.exitValue()
  4. Si waitFor() retourne false : timeout, appelez process.destroyForcibly() et lancez TimeoutException

Test de timeout : Lancez sleep 60 avec un timeout de 2 secondes pour vérifier la gestion.