Exécution synchrone d'un programme console dans une application C#.
Date de publication : 30/01/2005
Par
Jean-Alain Baeyens (autres articles)
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.
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
A. En utilisant un appel explicit à la gestion des événements
B. En utilisant le gestion des événement
VIII. Récupérer des informations affichées par le processus externe
-A. En utilisant un timer
-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ées à
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 à 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.
A. En utilisant un appel explicit à 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();
winStop.Show();
ProcessStartInfo processInfo = new ProcessStartInfo(@"d:\Arj.exe");
processInfo.WindowStyle = ProcessWindowStyle.Normal;
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;
Process arjProcess = Process.Start(processInfo);
while (! arjProcess.HasExited && ! winStop.Canceled)
{
arjProcess.WaitForExit(100);
Application.DoEvents();
}
if (winStop.Canceled)
{
arjProcess.Kill();
arjProcess.WaitForExit();
}
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.
B. En utilisant le gestion des événement
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 ;
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;
arjProcess.EnableRaisingEvents = true;
arjProcess.Exited += new System.EventHandler(this.arjEnded);
arjProcess.Start();
winStop.ShowDialog();
if ( ! arjProcess.HasExited )
{
arjProcess.Kill();
arjProcess.WaitForExit();
}
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. A 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 |
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 à échouée.");
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.
-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 ;
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;
processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;
processInfo.RedirectStandardOutput = true;
arjProcess = new Process();
arjProcess.StartInfo = processInfo;
arjProcess.EnableRaisingEvents = true;
arjProcess.Exited += new System.EventHandler(this.arjEnded);
arjProcess.Start();
timer.Start();
winStop.ShowDialog();
timer.Stop();
if ( ! arjProcess.HasExited )
{
arjProcess.Kill();
arjProcess.WaitForExit();
}
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 tout les dixièmes de seconde
mais le processus peut écrire plus de caractères dans le même
lapse de temp. 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 tiens probablement au fait que le
timer est à nouveau déclanché 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.
-B. En utilisant un thread
Nous sommes donc contraint 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 ;
winStop = new CancelForm();
Thread readInfo = new Thread(new ThreadStart(this.readOutput));
ProcessStartInfo processInfo = new ProcessStartInfo("Arj.exe");
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;
processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;
processInfo.RedirectStandardOutput = true;
arjProcess = new Process();
arjProcess.StartInfo = processInfo;
arjProcess.EnableRaisingEvents = true;
arjProcess.Exited += new System.EventHandler(this.arjEnded);
arjProcess.Start();
readInfo.Start();
winStop.ShowDialog();
if ( ! arjProcess.HasExited )
{
arjProcess.Kill();
arjProcess.WaitForExit();
}
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;
Thread readInfo = new Thread(new ThreadStart(this.readOutput));
winStop = new CancelForm();
winStop.Show();
ProcessStartInfo processInfo = new ProcessStartInfo(@"d:\Arj.exe");
processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;
processInfo.RedirectStandardOutput = true;
processInfo.Arguments = "e -y " + arjFileName + " " + outputPath;
arjProcess = Process.Start(processInfo);
readInfo.Start();
while (! arjProcess.HasExited && ! winStop.Canceled)
{
arjProcess.WaitForExit(100);
Application.DoEvents();
}
if (winStop.Canceled)
{
arjProcess.Kill();
arjProcess.WaitForExit();
}
winStop.Close();
exitCode = arjProcess.ExitCode ;
arjProcess.Close();
return exitCode ;
}
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];
}
} |
 |
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
dotnet 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.


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 oeuvre intellectuelle protégée par les droits d'auteurs. Copyright ©
. 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'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts.
Cette page est déposée à la
SACD.