Accueil > Tutoriels > Démarrer une autre application depuis un programme C#

Démarrer une autre application depuis un programme C#

Le .NET Framework propose plusieurs classes et objets pour gérer d’autres programmes ou processus. On peut, par exemple, démarrer et superviser une autre application depuis un programme .NET. Ce type d’opération est souvent utilisé pour bénéficier des fonctions proposées par d’autres applications, ou pour centraliser et gérer les informations des processus sur une machine.

Voici une présentation (ou un rappel) des fonctions essentielles disponibles dans le .NET Framework et des éléments importants à ne pas oublier pour gérer le cycle de vie d’une autre application ou d’un processus.

 

 

Instructions de base

 

Lancer un autre exécutable à partir du code C# d’une application consiste en fait à démarrer un nouveau processus lié à ce nouveau programme. Pour cela on utilise la classe Process située dans l’espace de nom System.Diagnostics. Le plus simple ensuite est d’utiliser la méthode statique Start en fournissant le chemin d’accès de l’exécutable à démarrer :

// Lancement de l'application Notepad
Process notepad = Process.Start("notepad.exe");

Par défaut, le nouveau programme se chargera dans une nouvelle fenêtre. Si la cible est une application console (programme .NET, commande DOS, batch, etc.), une nouvelle fenêtre système apparaitra alors pour y afficher les informations remontées lors de l’exécution. Cette fenêtre persistera tant que le processus sera en vie.

Le code ci-dessus n’est toutefois pas très pratique lorsque l’on souhaite disposer d’un contrôle plus fin sur l’application, car on a finalement très peu d’options pour configurer le processus avant son lancement. Le mieux est donc d’utiliser un objet ProcessStartInfo prévu à cet effet :

ProcessStartInfo notepadStartInfo = new ProcessStartInfo("notepad.exe");

// Méthode 1
Process notepad = Process.Start(notepadStartInfo);

// Ou méthode 2
Process notepad = new Process();
notepad.StartInfo = notepadStartInfo;
notepad.Start();

 

A noter qu’il est tout à fait possible de démarrer un processus en spécifiant un type de fichier autre qu’un exécutable, comme un fichier texte ou une image. Dans ce cas, le processus sera démarré avec l’application associée par défaut à l’extension du fichier. Pour reprendre la documentation MSDN, il s’agit d’une fonction équivalente à la commande « Run » du menu Démarrer de Windows. Par contre, cela ne fonctionnera que si le « shell » du système d’exploitation est utilisé pour créer le nouveau processus (car c’est lui qui va récupérer le programme associé à l’extension du fichier). En d’autres termes, la propriété UseShellExecute du ProcessStartInfo doit être à true (valeur par défaut).

 

N’oubliez pas que, contrairement à la création de threads, les processus créés ne sont pas détruits lorsque l’application parente est fermée. Ils continuent de fonctionner sur la machine. Nous verrons comment surveiller ou provoquer l’arrêt d’un processus un peu plus loin.

 

Paramétrage du nouveau processus

 

Comme nous l’avons vu un peu plus haut, la méthode appelée pour créer un nouveau processus est la méthode Start. Cependant, la classe Process utilise en interne deux méthodes bien distinctes pour la création des processus : L’une d’elles fait appel à la méthode native ShellExecuteEx de la dll système shell32.dll, et l’autre utilise la méthode CreateProcess de la librairie kernel32.dll. Cette différence peut avoir un impact important sur les options de démarrage des processus et leurs effets.

Pour déterminer la méthode utilisée, il suffit de définir la propriété UseShellExecute. Lorsque cette propriété vaut true, c’est la librairie shell32.dll qui est utilisée. Dans le cas contraire, c’est le kernel32 qui est appelé. Certaines options avancées ne seront donc disponibles que si la seconde méthode est employée pour créer le processus. Autrement dit, uniquement si UseShellExecute est mis à false. C’est le cas par exemple si l’on souhaite que le nouveau processus soit caché, sans que de nouvelles fenêtre (console ou Windows) ne soit créées.

 

Suppression de la fenêtre : UseShellExecute et CreateNoWindow

Si l’on souhaite au sein d’une application graphique appeler une commande externe pour effectuer un traitement, l’affichage des habituelles fenêtres à fond noir des applications console n’est pas forcément du meilleur effet. Heureusement, il est possible de les masquer en définissant quelques options avant de démarrer le nouveau processus. Pour cela, la première étape est de désactiver la propriété UseShellExecute de l’objet ProcessStartInfo :

ProcessStartInfo processStartInfo = new ProcessStartInfo(fileName, arguments);
processStartInfo.UseShellExecute = false;

 

Les autres options à définir vont ensuite dépendre du type d’application (parente et enfant). Dans le cas où les deux programmes sont des applications console, le code ci-dessus sera suffisant. Par contre, dans le cas où on a au moins une application Windows, ce ne sera pas suffisant : il faudra aussi activer la propriété CreateNoWindows (qu’il aurait été plus clair d’appeler « CreateWindow »).

 

Dossier de travail (working directory)

Le dossier de travail (ou working directory) est un des paramètres les plus importants à définir pour le nouveau processus. Généralement, celui-ci correspondra au dossier de travail de l’application parente, ce qui peut poser problème si le nouveau processus lance un programme qui a l’habitude de lire des fichiers (configurations, librairies, etc.) dans le dossier où est situé son fichier exécutable. Le dossier d’exécution peut se définir à l’aide de la propriété WorkingDirectory.

 

Attente et arrêt du processus

 

Attente synchrone

Lorsqu’il s’agit de démarrer une application externe pour effectuer un traitement précis, il est intéressant de mettre en attente l’application parente, et éventuellement d’ajouter quelques instructions pour gérer les problèmes rencontrés (erreur lors du traitement, délai trop long, etc.). La classe Process possède plusieurs méthodes et propriétés utiles comme WaitForExit, ExitCode ou Kill (CloseMainWindows pour une application graphique) :

Voici un exemple de code d’attente et de fermeture d’un processus :

Process newProcess = new Process();

try
{
    // Lancement du processus
    newProcess = Process.Start(processStartInfo);

    // Attente synchrone de la fin du traitement (maximum 1 minute)
    bool completed = newProcess.WaitForExit(60000);

    if(!completed)
    {
        // Temps d'attente dépassé : on tue le processus
        if (!newProcess.HasExited)
            newProcess.Kill();

        throw new Exception("Process not completed");
    }
    else if(newProcess.ExitCode != 0)
    {
        // Le processus a rencontré un problème et s'est arrêté
        throw new Exception("Exit code : " + newProcess.ExitCode);
    }
    //Traitement OK
}
catch(Exception ex)
{
    // Code général concernant le traitement des erreurs
}
finally
{
    // Libération des ressources
    newProcess.Close();
}

 

A noter que l’arrêt d’un processus ne signifie pas que les ressources de l’objet Process sont libérées. Tant que la méthode Close n’a pas été appelée, il est possible de récupérer certaines informations comme le code de sortie ou les flux. Pensez toutefois à ne pas oublier d’appeler cette méthode Close dès que le processus n’est plus nécessaire afin de libérer les ressources (à mettre idéalement dans le bloc finally d’un try..catch).

 

Attente asynchrone

Pour éviter de bloquer l’application parente, il est possible d’attendre la fin du processus créé en s’abonnant à l’événement ProcessExited. Ainsi, il n’est plus nécessaire de vérifier régulièrement le statut de ce processus : celui-ci déclenchera un événement lors de son arrêt. Il suffit ensuite de traiter cette information dans l’application parente.

Avant d’utiliser cette méthode, il est nécessaire d’activer le traitement des événements dans l’instance de la classe Process avec la propriété EnableRaisingEvents :

// Activation de l'envoi des événements
process.EnableRaisingEvents = true;

 

On peut ensuite s’abonner à cet événement et ajouter le code de traitement de l’arrêt du processus dans une méthode (ProcessExited dans cet exemple) :

// On s'abonne à l'événement Exited
process.Exited += new EventHandler(ProcessExited);
/// <summary>
/// Traitement de l'arrêt du processus
/// </summary>
private void ProcessExited(object sender, EventArgs e)
{
    // Mettre ici le code utile
}

 

Bien que ce code paraisse simple, il y a cependant une chose importante à savoir : même si le programme ne crée aucun thread supplémentaire, l’événement peut être envoyé à partir d’un thread différent du thread principal. En reprenant le code ci-dessus, si on tentait d’actualiser un contrôle graphique (un TextBox par exempl) dans la méthode ProcessExited, on aurait de fortes chances de déclencher une exception « Cross-Thread » InvalidOperationException, survenant lorsque l’on tente de modifier un contrôle à partir d’un thread différent du thread graphique (ou GUI pour l’interface). Bien qu’il soit possible de désactiver le déclenchement de cette exception, il est préférable d’éviter correctement le problème en utilisant les méthodes Invoke ou BeginInvoke :

private void ProcessExited(object sender, EventArgs e)
{
    // Invocation d'une méthode du thread graphique
    this.Invoke(new MethodInvoker(this.ProcessDisabled));
}

private void ProcessDisabled()
{
    this.textBox1.Text = "Process Exited";
}

 

Pour plus d’informations sur le sujet, vous pouvez lire cet article sur MSDN : Comment : faire des appels thread-safe aux contrôles Windows Forms.

 

Conclusion

 

Démarrer et arrêter un nouveau processus à partir d’un programme .NET est donc une opération assez simple, mais qui nécessite tout de même faire attention à la fermeture des processus créés, notamment lorsque l’application parente est fermée.

Beaucoup d’autres opérations intéressantes peuvent être effectuées sur les processus, comme le suivi régulier de la consommation mémoire, ou de l’activité CPU. Il est possible aussi de récupérer les informations envoyées habituellement par les applications console (avec Console.WriteLine par exemple) en effectuant une « redirection des flux », mais ce point sera détaillé plus tard dans un prochain article !

 

  1. Pas encore de commentaire
  1. Pas encore de trackbacks