Accéder au contenu principal

4min.

Comprendre (enfin) les TTY et PTY avec le composant Process de Symfony

Vous est-il déjà arrivé de lancer une commande (composer, rsync ou une commande Symfony) directement dans votre terminal pour y admirer de jolies barres de progression colorées, mais de constater que cette même commande, une fois exécutée via un script PHP, perdait soudainement tout son formatage ?

C’est un grand classique lorsque l’on utilise le composant Process de Symfony. Pour comprendre l’origine de ce comportement (et surtout comment y remédier), il faut plonger un instant dans la façon dont Linux gère les flux et les terminaux. Rassurez-vous, c’est plus simple qu’il n’y paraît.

Section intitulée retour-aux-bases-les-flux-standardsRetour aux bases : les flux standards

Sous Linux, chaque processus dispose par défaut de trois flux standards, identifiés par des descripteurs de fichiers (file descriptors) :

  • 0 : STDIN (l’entrée standard)
  • 1 : STDOUT (la sortie standard)
  • 2 : STDERR (la sortie d’erreur standard)

Ce qu’il faut retenir, c’est que ces flux se comportent comme de simples tuyaux. Ce qui se trouve au bout du tuyau détermine le comportement du programme. Généralement, on rencontre trois scénarios :

  1. Un fichier : Par exemple, lorsque vous redirigez une sortie avec ls > file.txt ;
  2. Un PTY / TTY (Terminal) : Lorsque vous exécutez la commande directement devant votre écran ;
  3. Un pipe (tube) : Lorsque vous enchaînez des commandes (ls | grep php) ou que vous lancez un sous-processus de manière programmatique.

Section intitulée le-quot-probleme-quot-de-l-execution-programmatiqueLe « problème » de l’exécution programmatique

Par défaut, quand vous utilisez le composant Process pour lancer une commande, Symfony utilise des pipes (notre troisième scénario).

Le programme exécuté (prenons Composer) est intelligent : il analyse ce qui se trouve au bout du tuyau de sa sortie standard (STDOUT). S’il détecte un pipe au lieu d’un terminal, il en déduit qu’il est exécuté par une machine ou un script. Pour éviter de polluer d’éventuels fichiers de logs avec des caractères invisibles (les fameux codes ANSI qui génèrent les couleurs) ou des barres de progression illisibles, il bascule automatiquement en mode « texte brut ».

C’est pour cette raison exacte que vos couleurs disparaissent.

Section intitulée la-methode-code-settty-true-code-le-lien-directLa méthode setTty(true) : le lien direct

Le composant Process propose une première solution avec la méthode setTty(true).

En l’activant, vous branchez directement les flux de votre sous-processus sur le vrai terminal de votre système (celui depuis lequel vous avez lancé votre script PHP).

La conséquence : Les couleurs et les animations sont de retour. La commande s’affiche exactement comme si vous l’aviez tapée vous-même. La limite : Puisque le flux est branché directement sur votre écran, votre script PHP n’y a plus accès. La synchronisation automatique fait que l’affichage est immédiat, mais il devient impossible de capturer la sortie avec un $process->getOutput() pour l’inspecter.

Section intitulée la-magie-de-code-setpty-true-code-l-illusion-parfaiteLa magie de setPty(true) : l’illusion parfaite

C’est ici qu’interviennent les Pseudo-Terminaux (PTY).

Lorsque vous utilisez setPty(true), vous demandez au système de créer un terminal émulé de toutes pièces. Un PTY fonctionne comme un duo composé d’un contrôleur et d’un terminal virtuel :

  1. Le script PHP (via Symfony Process) agit comme le contrôleur.
  2. Le processus enfant (votre commande) est branché sur le terminal virtuel émulé (qui prend la forme d’un fichier dynamique, souvent /dev/pts/X).

Pour le processus enfant, l’illusion est totale. Il détecte bien un terminal au bout de son tuyau et génère donc ses couleurs et ses barres de progression interactives.

Côté PHP, en tant que « contrôleur » du PTY, la donne change : l’envoi vers l’écran n’est plus automatique (il agit comme un buffer). C’est à vous de lire ce qui sort du terminal virtuel. Vous retrouvez ainsi le meilleur des deux mondes : vous forcez le programme à conserver son affichage riche, tout en gardant la capacité d’intercepter et de manipuler le flux sortant directement dans votre code PHP.

Section intitulée demo-timeDémo time !

On commence avec une commande Symfony qui affiche, si l’exécuteur le permet, des choses en couleur :

#!/usr/bin/env php
<?php
require __DIR__.'/vendor/autoload.php';

use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SingleCommandApplication;

new SingleCommandApplication()
    ->setCode(function (OutputInterface $output): int {
        $output->writeln('<info>Hello World!</info>');
        $output->writeln('<comment>This is a single command application.</comment>');
        $output->writeln('<error>Goodbye!</error>');

        return 0;
    })
    ->run();

Ensuite, nous exécutons cette commande Symfony, avec le composant Process. En fonction des arguments, nous activons ou non PTY ou TTY :

<?php

require __DIR__.'/vendor/autoload.php';

$process = new Symfony\Component\Process\Process([__DIR__ . '/console.php']);

$process->setTty(($argv[1] ?? '') === 'tty');
$process->setPty(($argv[1] ?? '') === 'pty');

$process->mustRun();

echo "\nOutput captured:\n";

dump($process->getOutput());

Et voici le résultat :

Alt text

Section intitulée en-resumeEn résumé

Si vous construisez des outils en ligne de commande ou des workers asynchrones en PHP :

  • Par défaut (Pipes) : À privilégier pour les tâches de fond où la sortie doit être parsée ou logguée proprement, sans caractères d’échappement ;
  • setTty(true) : Idéal si vous voulez simplement déléguer l’affichage et l’interactivité à l’utilisateur, sans avoir besoin d’analyser la sortie côté PHP ;
  • setPty(true) : La solution de choix pour forcer un affichage riche (couleurs, animations) tout en conservant le contrôle du flux sortant dans votre script.

Commentaires et discussions

Ces clients ont profité de notre expertise