Exécution synchrone d'un programme console dans une application C#

Jean-Alain Baeyens

Lorsque nous avons besoin d'exécuter un programme externe, surtout un programme console, nous devons généralement attendre le résultat de cette exécution avant de poursuivre notre traitement principal. Il s'agit d'une exécution synchrone. Il est également intéressant de connaître le résultat de cette exécution. Nous allons voir dans cet article comment réaliser une exécution synchrone et comment récupérer non seulement le code de retour, mais également du texte affiché par le programme console.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Exécution synchrone d'un programme console dans une application C#

- Auteur : Jean-Alain Baeyens

- Parution : 30 janvier 2005

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005 Jean-Alain Baeyens. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Remerciements

- I. Introduction

- II. Exécution en mode asynchrone

- III. Exécution en mode synchrone

- IV. Rendre l'exécution invisible à l'utilisateur

- V. La récupération du code de retour

- VI. Libérer la mémoire au plus vite

- VII. L'arrêt provoqué par l'utilisateur

- VII-A. En utilisant un appel explicite à la gestion des événements

- VII-B. En utilisant la gestion des événements

- VIII. Récupérer des informations affichées par le processus externe

- VIII-A. En utilisant un timer

- VIII-B. En utilisant un thread

- IX. Une mauvaise idée

- X. Conclusion

Remerciements

L'idée de cette publication m'est venue à la suite d'un post sur le forum. Je remercie toutes les personnes ayant participé à ce post. Je remercie également Sébastien Doeraene pour la relecture du document avant publication.

I - Introduction

Pour illustrer l'exécution synchrone d'un programme console depuis une application C#, nous allons construire une méthode qui permettra de décompacter un fichier précédemment comprimé avec Arj. Arj est un ancien programme de compression. Le principe peut évidemment être adapté à d'autres applications consoles. J'ai utilisé la version 3.10a d'Arj. Nous allons également voir comment récupérer des informations données par le programme console dans notre application. Il ne s'agit pas uniquement de vous expliquer une façon de faire, mais nous allons voir pas à pas le processus avec différentes approches pour chaque pas. Cette méthode nous permettra de mettre en lumière les problèmes liés à chaque manière de traiter le problème et d'aboutir à une bonne solution. Cela doit également aboutir à une meilleure compréhension de la solution.

Nous allons construire une classe nommée Arj et qui contiendra uniquement la méthode UnArj. Dans un premier temps, nous allons déclarer la classe abstraite et la méthode statique.

II - Exécution en mode asynchrone

Pour exécuter un programme externe, une seule ligne de commande suffit. DotNet met à notre disposition la classe Process.

Classe réalisant l'exécution d'un programme externe

public abstract class Arj
{
public static void UnArj (string arjFileName, string outputPath)
{
Process.Start("arj.exe");
}
}

Le problème est évident, notre application console attend des paramètres. Le premier réflexe est d'ajouter les paramètres à la ligne de commande.

Process.Start("arj.exe e -y " + arjFileName + " " + outputPath);

Cette façon de faire entraîne la réception du message « Cannot find file specified ». Nous devons donc utiliser un autre constructeur.

Classe réalisant l'exécution d'un programme externe avec passage de paramètres

public static void UnArj (string arjFileName, string outputPath)
{
Process.Start("arj.exe","e -y " + arjFileName + " " + outputPath);
}

Maintenant Arj (notre programme console) s'exécute correctement. Toutefois, notre programme appelant n'attend pas la fin de l'exécution d'Arj pour poursuivre. Il s'agit d'une exécution asynchrone.

III - Exécution en mode synchrone

Pour résoudre ce problème, nous devons utiliser la méthode WaitForExit pour signaler à notre programme d'attendre la fin du processus externe.

Le minimum pour exécuter une application en mode synchrone

public static void UnArj (string arjFileName, string outputPath)
{
Process arjProcess = Process.Start("arj.exe","e -y " + arjFileName + " "
+ outputPath);
arjProcess.WaitForExit();
}

Pour ma part, je préfère utiliser un objet de la classe ProcessStartInfo pour paramétrer mon processus. Il offre plus de contrôle sur l'exécution du processus. Pour le même résultat, notre code devient :

Utilisation de ProcessInfo

public static void UnArj (string arjFileName, string outputPath)
{
ProcessStartInfo processInfo = new ProcessStartInfo("Arj.exe");
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;
Process arjProcess = Process.Start(processInfo);
arjProcess.WaitForExit();
}

IV - Rendre l'exécution invisible à l'utilisateur

Nous pouvons cacher la fenêtre d'exécution d'Arj en intercalant la ligne de code suivante.

processInfo.WindowStyle = ProcessWindowStyle.Hidden;

L'exécution du programme externe est maintenant invisible pour l'utilisateur.

Si pour un motif ou un autre, le programme console attend l'intervention de l'utilisateur, votre programme va rester à l'arrêt sans que l'utilisateur sache pourquoi. Le paramètre -y dans la commande d'Arj permet de répondre « Yes » à toutes les questions.

V - La récupération du code de retour

Il sera probablement utile dans la suite du programme de savoir si l'application console a retourné un code d'erreur. Pour le récupérer, nous utiliserons la propriété ExitCode.

Utilisation d'ExitCode

public static int UnArj (string arjFileName, string outputPath)
{
ProcessStartInfo processInfo = new ProcessStartInfo("Arj.exe");
processInfo.WindowStyle = ProcessWindowStyle.Hidden;
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;
Process arjProcess = Process.Start(processInfo);
arjProcess.WaitForExit();
return arjProcess.ExitCode;
}

VI - Libérer la mémoire au plus vite

Si vous souhaitez récupérer directement la mémoire allouée pour maintenir les informations sur le process, vous devez utiliser la méthode Close.

Utilisation de Close

public static int UnArj (string arjFileName, string outputPath)
{
int exitCode;
ProcessStartInfo processInfo = new ProcessStartInfo("Arj.exe");
processInfo.WindowStyle = ProcessWindowStyle.Hidden;
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;
Process arjProcess = Process.Start(processInfo);
arjProcess.WaitForExit();
exitCode =  arjProcess.ExitCode;
arjProcess.Close();
return exitCode;
}

Cela nous oblige à stocker le code retourné dans une variable.

VII - L'arrêt provoqué par l'utilisateur

Le problème de cette version est que si le programme console ne rend pas la main, votre programme ne peut poursuivre et il ne vous reste plus qu'à le stopper violemment.

Pour éviter ce problème, il est possible d'introduire une fenêtre permettant à l'utilisateur d'annuler l'opération en cours.

Nous allons donc créer la fenêtre suivante :

Le problème à résoudre est que comme notre programme est en attente, il ne peut traiter les événements et donc il sera impossible d'effectuer le Stop.

Une première solution consiste à exécuter la fenêtre dans un thread séparé. Bien que cette solution soit possible, je ne l'ai pas retenue, car il me paraissait anormal de créer un thread en parallèle d'un programme que l'on a volontairement arrêté et cela pour attendre la fin d'une autre tâche. D'autant que d'autres voies sont à explorer.

VII-A - En utilisant un appel explicite à la gestion des événements

Il serait simple de l'introduire comme ceci :

Utilisation de DoEvents

public static int UnArj (string arjFileName, string outputPath)
{
int exitCode;
CancelForm winStop = new CancelForm();

// on utilise show et non showdialog pour que le processus se poursuive
winStop.Show();
ProcessStartInfo processInfo = new ProcessStartInfo(@"d:\Arj.exe");
processInfo.WindowStyle = ProcessWindowStyle.Normal;
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;
Process arjProcess = Process.Start(processInfo);

// on boucle tant que le processus n'est pas terminé ou que l'on n'a pas
// demandé l'arrêt.
while (! arjProcess.HasExited && ! winStop.Canceled)
{
// on attend la fin du processus pendant 100 millisecondes
arjProcess.WaitForExit(100);
// on traite la file des messages
Application.DoEvents();
}

// on vérifie qu'un arrêt est demandé
if (winStop.Canceled)
{
// on arrête le processus
arjProcess.Kill();

// on attend qu'il soit complètement arrêté
arjProcess.WaitForExit();
}

// on ferme la fenêtre
winStop.Close();
exitCode =  arjProcess.ExitCode ;
arjProcess.Close();
return exitCode ;
}

Il y a deux cas de figure.

- l'utilisateur attend le déroulement de la décompression. Dans ce cas, à la fin du processus, la boucle d'attente est interrompue par la condition HasExited. Le programme continue. winStop.Canceled est false. Nous fermons la fenêtre ;

- l'utilisateur presse le bouton Stop. La boucle d'attente est interrompue par la condition Canceled, le programme continue. winStop.Canceled est true, nous mettons fin au process au moyen de la commande Kill. Nous devons encore patienter pendant l'opération d'arrêt du processus avant de pouvoir accéder à ExitCode. Nous fermons la fenêtre.

VII-B - En utilisant la gestion des événements

Pour ma part je n'apprécie que moyennement l'idée de demander explicitement à intervalle régulier le traitement de la file des événements. Pour éviter cela, c'est le processus qui doit avertir le programme appelant qu'il a fini. Pour ce faire, nous allons devoir modifier notre code.

La nouvelle Class Arj

public class Arj
{
private CancelForm winStop;

public int UnArj(string arjFileName, string outputPath)
{
int exitCode ;

// affichage de la fenêtre d'arrêt
winStop = new CancelForm();

ProcessStartInfo processInfo = new ProcessStartInfo("Arj.exe");
processInfo.WindowStyle = ProcessWindowStyle.Hidden;
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;
Process arjProcess = new Process();
arjProcess.StartInfo = processInfo;

// on utilise l'événement pour savoir quand le processus est terminé
arjProcess.EnableRaisingEvents = true;
arjProcess.Exited += new System.EventHandler(this.arjEnded);
arjProcess.Start();
winStop.ShowDialog();

// on vérifie si le processus est fini
// Si ce n'est pas le cas, l'utilisateur a appuyé sur stop
// et il faut fermer le processus
if ( ! arjProcess.HasExited )
{
arjProcess.Kill();
arjProcess.WaitForExit();
}

// après le close, les infos sur le processus ne seront plus
// disponible. La mémoire allouée pour conserver ces infos
// est rendue disponible
exitCode = arjProcess.ExitCode;
arjProcess.Close();
return exitCode;
}

private void arjEnded(object sender, System.EventArgs e)
{
winStop.Close();
}
}

Nous allons maintenant détailler les modifications et leurs motifs :

- la classe n'est plus abstraite. L'instance de la fenêtre permettant de stopper l'opération doit être déclarée en dehors de la méthode, car elle est accédée depuis deux méthodes. La classe doit dès lors être instanciable pour permettre la cohabitation de deux appels. Pour les mêmes motifs, la méthode perd son attribut static ;

- arjProcess n'est plus instancié par la méthode statique Start, mais bien par son constructeur. Nous avons besoin de définir plus d'informations pour l'exécution. Il est donc nécessaire de séparer l'instanciation d'arjProcess de son exécution (Start) ;

- EnableRaisingProcess indique au processus qu'il devra exécuter l'événement Exited après l'exécution de la tâche. À la ligne suivante, nous associons à l'événement la méthode qui devra être déclenchée. On affiche après avoir lancé le processus, la boîte avec ShowDialog. Le programme attend alors que l'utilisateur appuie sur Stop. Mais le programme est capable de gérer les événements, il n'est pas occupé dans une boucle de votre code.

Il y a alors deux cas de figure :

- l'utilisateur attend le déroulement de la décompression. Dans ce cas, à la fin du processus, la méthode arjEnded est automatiquement appelée et ferme la boîte de dialogue. Le programme continue. HasExited nous informe que le processus est fini et nous pouvons terminer comme précédemment ;

- l'utilisateur presse le bouton Stop. La fenêtre de dialogue se ferme, HasExited nous informe que le processus est en cours, nous y mettons fin au moyen de la commande Kill. Nous devons encore patienter pendant l'opération d'arrêt du processus avant de pouvoir accéder à ExitCode.

La procédure appelante doit évidemment être également modifiée.

Programme appelant

// On récupère le code retourné par arj et on fait un traitement différencié.
// Pour l'exercice, on traite uniquement réussi ou échec, mais
// vous pouvez compléter le switch pour effectuer un traitement distinct
// pour chaque code d'erreur.
Arj arj = new Arj();
switch (arj.UnArj(@"d:\test.arj", @"d:\"))
{
case -1:
{
MessageBox.Show("La décompression a été annulée par l'opérateur");
break;
}
case 0:
{
MessageBox.Show("Décompression terminée.");
break;
}
default:
{
MessageBox.Show("La décompression a échoué.");
break;
}
}

VIII - Récupérer des informations affichées par le processus externe

Pour que le travail soit réellement complet, nous devrions informer l'utilisateur de l'évolution du processus. Pour cela nous devons rediriger sa sortie. Ce qui est réalisé au moyen des instructions suivantes :

processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;
processInfo.RedirectStandardOutput = true;

Remarquons au passage que la propriété CreateNoWindow nous dispense de faire

processInfo.WindowStyle = ProcessWindowStyle.Hidden;

Nous aurions pu faire cela dès le départ.

Notre programme attend la fin de la boîte de dialogue. Nous devons pourtant exécuter du code pour récupérer le texte affiché par le processus.

VIII-A - En utilisant un timer

Une possibilité est d'utiliser un Timer qui va appeler une procédure.

Affichage des informations en utilisant un timer

public int UnArj(string arjFileName, string outputPath)
{
int exitCode ;

// affichage de la fenêtre d'arrêt
winStop = new CancelForm();
Timer timer = new Timer();
timer.Interval = 50;
timer.Tick += new EventHandler(readOutput);

ProcessStartInfo processInfo = new ProcessStartInfo("Arj.exe");
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;

// on déroute la sortie standard
processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;
processInfo.RedirectStandardOutput = true;

arjProcess = new Process();
arjProcess.StartInfo = processInfo;

// on utilise l'événement pour savoir quand le processus est terminé
arjProcess.EnableRaisingEvents = true;
arjProcess.Exited += new System.EventHandler(this.arjEnded);

arjProcess.Start();
timer.Start();
winStop.ShowDialog();

// On vérifie si le processus est fini.
// Si ce n'est pas le cas, l'utilisateur a appuyé sur stop
// et il faut fermer le processus
timer.Stop();
if ( ! arjProcess.HasExited )
{
arjProcess.Kill();
arjProcess.WaitForExit();
}

// Après le close, les infos sur le processus ne seront plus
// disponible. La mémoire allouée pour conserver ces infos
// est rendue disponible
exitCode = arjProcess.ExitCode;
arjProcess.Close();
return exitCode;
}

private void readOutput(object sender, System.EventArgs e)
{
if (! arjProcess.HasExited)
{
char[] buffer = new char[10];
int ind = arjProcess.StandardOutput.Read(buffer,0,10);
int i = ind - 1;
bool notFound = true;
char, car = '%';
while (i>=0 && notFound)
{
if (buffer[i]==,car)
{
notFound=false;
}
else
{
i--;
}
}
if (! notFound)
{
winStop.lblInfo.Text =
Convert.ToString(buffer[i-2])+Convert.ToString(buffer[i-1])+"%";
}
}
}

Cette solution semble efficace. Toutefois, elle est loin d'être parfaite. Selon la taille du buffer que vous allez lire et le délai que vous définissez pour votre Timer, l'affichage du pourcentage peut prendre du retard par rapport au pourcentage réel. Les raisons en sont fort simples, dans l'exemple on lit 50 caractères tous les dixièmes de seconde, mais le processus peut écrire plus de caractères dans le même laps de temps. Le dernier pourcentage lu n'est donc pas le dernier. La solution est de lire une chaîne plus grande, mais dans ce cas, l'affichage s'arrête au bout d'un moment. Là, l'explication est moins évidente, mais tient probablement au fait que le timer est à nouveau déclenché avant que le bloc de lecture soit rempli. De plus, cela dépend de la vitesse d'exécution donc de votre ordinateur et de la taille du fichier.

VIII-B - En utilisant un thread

Nous sommes donc contraints de réaliser la lecture dans un thread. Finalement, notre code va s'en trouver beaucoup plus simple. Deux instructions suffisent pour créer et démarrer le thread. La lecture se faisant en permanence, il ne faut plus s'occuper de lire des segments entiers pour récupérer la fin. Il nous suffit de lire caractère après caractère ce qui simplifie grandement le traitement.

Affichage des informations en utilisant un thread

public int UnArj(string arjFileName, string outputPath)
{
int exitCode ;

// affichage de la fenêtre d'arrêt
winStop = new CancelForm();

// on crée le thread de lecture des infos
Thread readInfo = new Thread(new ThreadStart(this.readOutput));

ProcessStartInfo processInfo = new ProcessStartInfo("Arj.exe");
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;

// on déroute la sortie standard
processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;
processInfo.RedirectStandardOutput = true;

arjProcess = new Process();
arjProcess.StartInfo = processInfo;

// on utilise l'événement pour savoir quand le processus est terminé
arjProcess.EnableRaisingEvents = true;
arjProcess.Exited += new System.EventHandler(this.arjEnded);

arjProcess.Start();

// on démarre le thread de lecture
readInfo.Start();

// on affiche la fenêtre de dialogue
winStop.ShowDialog();

// On vérifie si le processus est fini.
// Si ce n'est pas le cas, l'utilisateur a appuyé sur stop
// et il faut fermer le processus. Le thread est fermé après l'arrêt
// du processus
if ( ! arjProcess.HasExited )
{
arjProcess.Kill();
arjProcess.WaitForExit();
}

// Après le close, les infos sur le processus ne seront plus
// disponible. La mémoire allouée pour conserver ces infos
// est rendue disponible
exitCode = arjProcess.ExitCode;
arjProcess.Close();
return exitCode;
}

private void arjEnded(object sender, System.EventArgs e)
{
winStop.Close();
}

private void readOutput()
{
char[] buffer = new char[1];
char car1 = ' ';
char car2 = ' ';
char, car = '%';
while (! arjProcess.HasExited)
{
arjProcess.StandardOutput.Read(buffer,0,1);
if (buffer[0] ==, car)
{
winStop.lblInfo.Text =
Convert.ToString(car1)+Convert.ToString(car2) + "%";
}
car1 = car2;
car2 = buffer[0];
}
}

IX - Une mauvaise idée

Pour ceux qui préfèrent la version avec DoEvents, voici le code modifié pour obtenir le même résultat.

public class Arj
{
private CancelForm winStop;
private Process arjProcess;

public int UnArj(string arjFileName, string outputPath)
{
int exitCode;

// on instancie un thread pour la lecture de l'affichage du processus
Thread readInfo = new Thread(new ThreadStart(this.readOutput));

// on utilise show et non showdialog pour que le processus se poursuive
winStop = new CancelForm();
winStop.Show();

// on défini le processus
ProcessStartInfo processInfo = new ProcessStartInfo(@"d:\Arj.exe");

// on déroute la sortie standard
processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;
processInfo.RedirectStandardOutput = true;
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;
arjProcess = Process.Start(processInfo);
readInfo.Start();

// on boucle tant que le processus n'est pas terminé et que l'opérateur
// n'a pas demandé l'arrêt du processus
while (! arjProcess.HasExited && ! winStop.Canceled)
{
arjProcess.WaitForExit(100);

// On traite la file des messages pour permettre des actions sur les
// fenêtres dont le stop.
Application.DoEvents();
}

// on vérifie s'il faut interrompre le processus
if (winStop.Canceled)
{
arjProcess.Kill();

// on attend que le processus soit bien terminé
arjProcess.WaitForExit();
}
winStop.Close();

// On enregistre le code de résultat, car le Close va rendre les
// propriétés de l'objet inaccessible
exitCode =  arjProcess.ExitCode ;
arjProcess.Close();
return exitCode ;
}

private void readOutput()
{
char[] buffer = new char[1];
char car1 = ' ';
char car2 = ' ';
char, car = '%';

// on lit le fichier de sortie tant que le processus n'est pas fini
while (! arjProcess.HasExited)
{
arjProcess.StandardOutput.Read(buffer,0,1);

// si le caractère lu est un % on affiche le pourcentage
if (buffer[0] ==, car)
{
winStop.lblInfo.Text =
Convert.ToString(car1)+Convert.ToString(car2) + "%";
}

// on conserve les 2 caractères lus précédemment avant d'effectuer
// une nouvelle lecture
car1 = car2;
car2 = buffer[0];
}
}

Je déconseille toutefois fortement cette façon de faire, car non seulement il ne s'agit pas de la bonne manière d'utiliser botnet, mais de plus, les performances sont très médiocres.

X - Conclusion

Finalement nous obtenons une méthode propre pour réaliser la gestion synchrone d'un processus externe tout en informant et en autorisant l'utilisateur à stopper le processus en cours. L'application est également correctement informée du résultat et peut prendre les mesures adéquates. Une programmation propre et dans l'esprit de dotnet rend non seulement votre code plus solide et plus lisible, mais également plus performant.