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.
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.
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.
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 :
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.
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.
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 :
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.
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.
// 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.
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.
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.