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

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 Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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
Sélectionnez

	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.

 
Sélectionnez

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
Sélectionnez
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
Sélectionnez

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
Sélectionnez

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.

 
Sélectionnez
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
Sélectionnez

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
Sélectionnez

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 :

Image non disponible

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
Sélectionnez

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.

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
Sélectionnez
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. 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
Sélectionnez

// 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 à é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 :

 
Sélectionnez
processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;
processInfo.RedirectStandardOutput = true;

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

 
Sélectionnez
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
Sélectionnez

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 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
Sélectionnez

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.

 
Sélectionnez
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 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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Liens utiles :
Faq : Comment lancer un processus ?
Faq : Multithreading
Les threads en C# par Olivier Brin
Télécharger les sources :
Code source avec utilisation d'un thread
Code source avec utilisation d'un timer (Déconseillé. Pour informations uniquement.)

  

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 © . 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.