[.NET] Récupérer la sortie d’un processus

Une question qui est fréquemment posée dans les forums de Microsoft : « Comment récupérer la sortie d’un processus ».
La méthode la plus simple sans se prendre la tête est d’exécuter un fichier de commande « .cmd » qui se contente de rediriger la sortie d’un processus vers un fichier. Il suffit ensuite d’exploiter ce fichier…

Le gros inconvénient de cette méthode (classifiée de « bricolage ») sont :

  • On doit passer par un fichier temporaire (problème d’espace sur le disque, performances de lecture et d’écriture,…etc).
  • On doit attendre la fin de l’exécution du processus pour pouvoir lire le fichier.
  • On doit modifier le fichier de commande si l’on veut changer le nom du fichier temporaire.

Pour éviter ce genre de problèmes, Windows propose lors de la création d’un processus de spécifier sur quel objet on souhaite « écrire » la sortie du processus (sur un fichier, à l’écran,…etc).
Il faut spécifier cela dans le paramètre « lpStartupInfo » de la fonction CreateProcess. Je ne vais pas m’étendre sur l’utilisation de cette fonction car cela sortirait du cadre .NET, mais sachez cependant que c’est cette fonction que vous devez paramétrer, lorsque vous devez rediriger la sortie d’un processus en utilisant une plate-forme de développement autre que .NET.

Sous .NET on utilise l’objet System.Diagnostics.Process qui représente un processus en cours d’exécution. Il est possible de lire la sortie d’un processus directement via la propriété StandardOutput.

Il existe cependant deux façons pour récupérer la sortie d’un processus :

  • De manière synchrone : C’est à dire que votre application .NET attend que des informations soient disponible en sortie. (Si il n’y a rien, l’application .NET est bloqué)
  • De manière asynchrone : .NET écoute les informations émisent par la sortie du processus tout en faisant tourner votre application .NET.

Récupérer la sortie d’un processus de façon synchrone

Après avoir lancé un processus via la méthode Process.Start() on peut utiliser la méthode StreamReader.ReadToEnd() pour obtenir toute la sortie du processus dans une variable de type string.

Un exemple pour mettre tout cela en œuvre :

D’abord il nous faut un fichier de commande qui produit de l’affichage en sortie. Ce fichier de commande sera intitulé « Processus.cmd » et se situera dans le répertoire « C:\Processus.cmd ».
J’ai choisi de prendre un fichier de commande plutôt qu’une autre application .NET afin de ne pas embrouiller les débutants entre 2 applications .NET, et surtout pour vous démontrer que l’on peut récupérer la sortie standard de n’importe quel type de processus, développé dans n’importe quel langage de programmation !

@ECHO OFF
ECHO Affichage d'une ligne...
ECHO Affichage d'une  autre ligne...
ECHO Affichage encore d'une autre ligne...

Avant d’allez plus loin, pour montrer le résultat que l’on souhaite récupérer, lancez l’invite de commande Windows.
Saisissez : « C:\Processus.cmd »

Vous devez voir le résultat suivant :

Résultats d'affichage de la sortie du programme de commandes

Ces 3 lignes, sont la sortie de notre processus que l’on souhaite récupérer dans une application .NET.

Maintenant je vous propose de télécharger un projet Windows Forms (à la fin de cet article) afin que l’on puisse exploiter la sortie de ce fichier de commande et l’afficher dans un TextBox.

Ouvrez le projet téléchargé, exécutez-le et cliquez sur le bouton « Lancer c:\processus.cmd ». Vous constatez que le TextBox contient le résultat voulu :

Récupération de la sortie du processus dans un TextBox

Ce code fonctionne parfaitement et attend la fin du processus pour récupérer la sortie. Cela peut vous poser des problèmes si votre processus est long et si vous ne souhaitez pas l’attendre.

Pour mettre en œuvre ce problème voici les commandes à mettre dans Processus.cmd :

@ECHO OFF
SET i=0
:BOUCLE_LA
SET /a i += 1
IF /i %i%  EQU 10000 GOTO FIN
ECHO Iteration %i% && GOTO BOUCLE_LA
:FIN

Ce programme se contente d’itérer une boucle de 0 à 10000 et d’afficher sur la sortie : « Iteration xxx ».
Relancez maintenant le programme .NET et cliquez sur le bouton, vous constatez 2 choses :

  • La console reste ouverte et n’affiche rien (C’est normal car le processus s’exécute, et la sortie n’est pas redirigée vers l’écran).
  • Votre programme .NET est bloqué.

Pour remédier à ce problème il existe 2 solutions :

  • Placer le code qui attend la sortie du processus dans un Thread.
  • Récupérer la sortie du processus de façon asynchrone.

Récupérer la sortie de façon synchrone dans un autre Thread

La première solution est facile à réaliser, il suffit de placer le code de OnLancerClick dans une méthode appelée Recup() par exemple.
Placez ensuite le code suivant dans la méthode OnLancerClick :

Thread t;

t = new  Thread(this.Recup);
 t.Start();
Dim t As Threading.Thread

t = New  Threading.Thread(AddressOf Recup)
 t.Start()

L’inconvénient d’utiliser un autre Thread, c’est qu’il faut faire une gymnastique particulière pour modifier les propriétés d’un contrôle Windows Forms (Cela fera l’objet d’un autre article). Il faut donc remplacer dans la méthode Recup() la ligne :

this.textBox.Text = s;
Me.textBox.Text = s

par :

this.Invoke(new SetTextHandler(this.SetText), s);
Me.Invoke(New SetTextHandler(AddressOf SetText), s)

et ajoutez le code suivant dans la classe Form1 :

private delegate void SetTextHandler(string s);

private void SetText(string s)
 {
 this.textBox.Text =  s;
 }
Private Delegate Sub SetTextHandler(ByVal s As  String)

Private Sub SetText(ByVal s As String)
 Me.textBox.Text =  s
 End Sub

Observez le résultat, vous constatez que votre application .NET n’est pas bloqué, mais elle attend tout le contenu de la sortie, c’est à dire la fin du processus lancé. On peut remédier à ce problème en lisant le flux de sortie « à la demande, c’est à dire ligne par ligne…

Récupérer la sortie de façon synchrone « à la demande »

Précédemment, vous avons vu la méthode ReadToEnd() qui permet de récupérer toute la sortie d’un processus, on peut aussi récupérer ligne par ligne la sortie d’un processus en utilisant la méthode ReadLine().

Cependant il faut savoir que cette méthode est synchrone car :

  • S’il existe déjà des lignes émises par le processus que vous n’avez pas lue (elle sont donc stockée dans un buffer interne), cette méthode vous renverra immédiatement une ligne, sans bloquer votre application.
  • Dans le cas contraire votre application .NET sera bloquée en attente d’au moins une ligne de la sortie standard du processus.

Récupérer la sortie de façon asynchrone

La récupération de la sortie de façon asynchrone est aussi simple à mettre en œuvre, on traite l’événement Process.OutputDataReceived et on appel la méthode Process.BeginOutputReadLine() pour démarrer « l’écoute ». Une fois que l’écoute est démarrée, .NET déclenche pour chaque ligne récupérée, l’événement OutputDataReceived.

ATTENTION : Cette méthode s’exécute sur Thread différent, ce qui pose le même problème vue précédemment, et nécessite une gymnastique particulière.

Reprenez donc le projet crée précédemment. Créons maintenant une méthode qui sera appelée automatiquement à chaque sortie effectuée par le processus :

private void SortieProcessus(object sendingProcess,  DataReceivedEventArgs outLine)
 {
 //Afficher une ligne de sortie de  "Processus.cmd" si non vide
 if (string.IsNullOrEmpty(outLine.Data) ==  false)
 this.Invoke(new SetTextHandler(this.SetText),  outLine.Data);
 }
Private Sub SortieProcessus(ByVal sendingProcess As Object,  ByVal outLine As DataReceivedEventArgs)
 'Afficher une ligne de sortie de  "Processus.cmd" si non vide
 If String.IsNullOrEmpty(outLine.Data) = False  Then
 Me.Invoke(New SetTextHandler(AddressOf Me.SetText),  outLine.Data)
 End If
 End Sub

Bien sûr cette méthode doit être du protoype System.Diagnostics.DataReceivedEventHandler.

Remplacez maintenant le contenu de la méthode SetText par :

private void SetText(string s)
 {
 this.textBox.AppendText(s);
 this.textBox.AppendText(Environment.NewLine);
 }
Private Sub SetText(ByVal s As String)
 Me.textBox.AppendText(s)
 Me.textBox.AppendText(Environment.NewLine)
 End  Sub

Cela permettra d’ajouter à la fin du TextBox tous les nouvelles lignes reçues.

Il faut ensuite se brancher sur l’événement Process.OutputDataReceived et appeler la méthode Process.BeginOutputReadLine(). Remplacez alors la ligne :

s = p.StandardOutput.ReadToEnd();
s = p.StandardOutput.ReadToEnd()

par :

p.OutputDataReceived += new  DataReceivedEventHandler(this.SortieProcessus);
 p.BeginOutputReadLine();
AddHandler p.OutputDataReceived, AddressOf  Me.SortieProcessus
 p.BeginOutputReadLine()

et supprimer bien évidemment la ligne suivante à la fin de la méthode OnLancerClick :

this.Invoke(new SetTextHandler(this.SetText), s);
Me.Invoke(new SetTextHandler(AddressOf Me.SetText), s);

Lancer le projet, vous constatez que la sortie s’affiche au fur et mesure dans le TextBox.

Que choisir entre synchrone et asynchrone ?

La réponse dépend bien évidemment de votre application.

  • Si vous souhaitez exécuter un processus et attendre sa fin pour obtenir sa sortie (par exemple l’exécution d’un compilateur), dans ce cas préférez la méthode synchrone.
  • Si vous souhaitez exécuter un processus tout en écoutant sa sortie et sans bloquer l’application .NET (par exemple l’exécution d’un script SQL), préférez dans ce cas la méthode asynchrone.

Une petite note concernant la méthode synchrone :
Nous avons vu que cette méthode bloque votre application .NET en attendant la fin du processus dans le cas de la méthode ReadToEnd() ou partiellement l’application s’il n’y a rien à récupérer dans le cas de la méthode ReadLine(). Cela pose un gros problème au niveau de l’affichage de votre application Windows Forms.
En effet, lorsque vous bloquez votre application, vous bloquez principalement le Thread d’affichage qui s’occupe du rendu graphique de votre application. Si votre processus met longtemps à se terminer, préférez dans de cas, l’utilisation de la solution synchrone dans un autre thread.

Conclusion

Il est très facile sous .NET de récupérer les sorties standards des processus lancés par la méthode System.Diagnostics.Process.Start() et cela de manière synchrone ou asynchrone.

A noter qu’il est possible aussi de rediriger la sortie des erreurs via la propriété StandardError, et l’entrée standard via la propriété StandardInput (le procédé pour ce dernier étant identique, à la différence que l’on écrit des informations au lieu de les lire)

Téléchargements

Publié dans la catégorie .NET Framework.
Tags : , , . TrackBack URL.