Application mono-instance et passage de paramètres

Résolu
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 - Modifié le 13 juil. 2019 à 20:55
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 - 30 juil. 2019 à 14:03
Bonjour,

Je développe actuellement une application multi-documents.
J'ai créé une association de fichier avec mon appli pour lancer l'appli quand on double-clique sur un fichier de ce type dans l'explorateur, et je traite les arguments de commande au démarrage de l'appli. Rien que du classique.

Par contre, lorsque l'on clique sur plusieurs fichiers sélectionnés à la fois, il s'ouvre autant de sessions de l'appli qu'il y a de fichiers. Ce n'est pas vraiment top pour une application multi-documents.

Je souhaiterais que le double-clic sur les fichiers ouvre une seule session de l'appli, et que les différents fichiers viennent se charger dans cette session.

J'ai trouvé pas mal de choses sur le net concernant les applis mono-instance, mais toutes ne font que bloquer les sessions suivantes, sans activer la session existante ni à fortiori lui passer les paramètres de ligne de commande.

Le truc le plus efficace que j'ai trouvé semble être ceci :
using System.Diagnostics;

    Process aProcess = Process.GetCurrentProcess();
    string aProcName = aProcess.ProcessName;
   
    if (Process.GetProcessesByName(aProcName).Length > 1)
    {
        Application.ExitThread();
    }


Je suppose que si l'appli est déjà lancée, le nombre de processes est de 2, et qu'on doit pouvoir à accéder à l'autre (le 0 ou le 1 ??). Mais comment lui passer les arguments de la ligne de commande de celui qui démarre ?

Si quelqu'un a déjà réalisé ce truc de ce genre en C#, je suis preneur d'un bon exemple...

PS : j'avais il a une quinzaine d'années réalisé ce passage de paramètres en VB6, mais au prix d'une gymnastique complexe : ouverture d'un port FTP en écoute par le première session, et envoi des paramètres à localhost sur ce port par les sessions suivantes. J'espère qu'on peut faire mieux en C#, avec quelque chose comme un partage de mémoire ou un mutex. Mais pour le moment, je ne sais pas faire ça.

8 réponses

Whismeril Messages postés 19014 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 13 avril 2024 654
14 juil. 2019 à 23:41
Bonsoir

peut-être en faisant un hook sur le double click.
https://stackoverflow.com/questions/12851010/detect-double-click-globally-in-windows
0
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 2
15 juil. 2019 à 10:33
Merci pour le lien, mais je n'ai pas bien compris la relation avec mon problème.

Le problème n'est pas tant le double-clic que la détection d'une instance existante, l'appel d'une fonction de cette autre instance avec des paramètres, et de s'auto-fermer ensuite.

Cela doit aussi fonctionner si on fait un clic droit sur un fichier puis "Ouvrir" ou "Ouvrir Avec". Ce n'est donc pas le fonctionnement de l'explorateur qui est en cause mais bien l'appli elle-même au lancement qui doit vérifier si une autre instance existe et lui passer la main avec le nom du fichier.

Ça doit bien pouvoir se faire puisque beaucoup de programmes le font : DreamWeaver, Word, Excel, Photoshop, etc.
0
Whismeril Messages postés 19014 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 13 avril 2024 654
15 juil. 2019 à 20:00
Ha oui j'avais pas pensé à "Ouvrir" et "Ouvrir Avec".
0
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 2
15 juil. 2019 à 21:03
Je crois que j'ai trouvé un article qui correspond à ce que je cherche.
Et comme je le préssentais, c'est à base de mutex, avec en plus des named pipes.
Mais c'est en anglais (non, en américain, c'est pire) et j'ai beaucoup de mal à assimiler les explications.
Je reprendrai cette discussion quand j’aurai compris et réussi à faire ma comm entre les instances.

Le post en question :
https://www.autoitconsulting.com/site/development/single-instance-winform-app-csharp-mutex-named-pipes/
0

Vous n’avez pas trouvé la réponse que vous recherchez ?

Posez votre question
Whismeril Messages postés 19014 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 13 avril 2024 654
15 juil. 2019 à 21:18
Ha ben je venais justement te proposer une solution avec un pipe
https://stackoverflow.com/questions/13806153/example-of-named-pipes

Sinon, tu peux aussi faire une communication en UDP sur localHost

Et là une solution avec de la mémoire partagée
https://www.codeproject.com/Articles/14740/Fast-IPC-Communication-Using-Shared-Memory-and-Int
0
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 2
Modifié le 16 juil. 2019 à 11:16
Ok, je vais regarder ça.
Ce n'est pas souvent qu'on me propose une pipe... ;-)

La deuxième proposition est ce que j'avais réalisé en VB6.
C'était dans une application professionnelle pour un client unique.
Pour une appli publique destinée à être diffusée, elle a un gros inconvénient : Le pare-feu de Windows bloque l'ouverture du port et demande l'autorisation à l'utilisateur.
Si ce dernier n'est pas administrateur, ça ne fonctionne pas.
Dans l'autre cas, l'affichage d'un message d'alerte du pare-feu inquiète l'utilisateur, qui généralement le refuse.
C'est pourquoi je préfère ne pas utiliser ce système.

Je vais commencer par voir les named pipes, dont le principe me plait assez.
Je reviendrai pour donner mes avancées.
Pour le moment il faut que je finalise le reste de l'appli, le problème présent étant un + facultatif.
0
Whismeril Messages postés 19014 Date d'inscription mardi 11 mars 2003 Statut Contributeur Dernière intervention 13 avril 2024 654
16 juil. 2019 à 12:34
UN pipe non mais!

Il y a du avoir une intervention de ton correcteur d’orthographe car dans ton premier message, il est écrit FTP, mais en remplacement par UDP ou TCP c’est plus compréhensible.
0
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 2
Modifié le 16 juil. 2019 à 13:56
Effectivement, c'est TCP qu'il fallait lire. De la comm entre sockets plus précisément.
Le correcteur d'orthographe a bon dos... C'était vraiment une faute de frappe.
Merci pour la correction.
0
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 2
Modifié le 29 juil. 2019 à 19:30
Ça avance...

Après m'être fait plusieurs entorses aux neurones avec les names pipes, j'ai fini par trouver un autre mode de communication entre processus : les Remoting Channels (IPC = Inter Process Channels).
J'ai trouvé des exemples dans la doc Microsoft ou autres, mais pas du tout adaptés à ce que je veux faire : c'était toujours des exemples TRÈS rustiques, et toujours le client qui recevait les infos. De plus, toujours en mode console avec blocage du serveur en écoute. Alors que moi, il faut que ce soit le serveur qui les reçoive, et qu'il continue de tourner sans bloquer sur la réception.

J'ai fait une maquette en mode console avec les exemples de Microsoft pour essayer de comprendre comment ça discute. Ça marchait plutôt bien, sauf que la discussion allait du serveur vers le client.

Dans l'exemple, la classe de communication n'avait qu'une propriété Get, dont la valeur était reçue par le client. J'ai eu l'idée de créer une propriété Set, et de la renseigner par le client => le serveur reçoit cette valeur ! Bingo.

Il me reste maintenant à intégrer ce code dans mon appli. Pas de problème pour le client (instances suivantes), qui va donc envoyer sa ligne de commande au serveur (première instance) qui pourra charger les fichiers.

Reste deux problèmes :
1 - Ne pas bloquer l'instance principale, mais rester cependant à l'écoute du canal IPC pendant toute la session. Si une instance globale de l'objet IPC ne convient pas, j'essaierai dans un thread séparé.
2 - Lorsque l’instance première (serveur) reçoit une liste de fichiers, il faut qu'elle se refasse passer au premier plan devant toutes les autres applications. Je n'ai pas encore essayé ça, mais je pense que je trouverai (BringToFront(), ou TopLevel On/Off ?). Si vous savez faire, un petit conseil sera le bienvenu.

Je joins ci-dessous le code de la maquette pour faire gagner du temps à ceux qui voudraient faire comme moi.
Pour faire fonctionner ça:
- Faire deux projets dans un solution : l'un IpcServeur et l'autre IpcClient, ajouter le code correspondant ainsi que la classe LoadFile dans chacun, et les compiler
- Lancer l'exécutable du serveur
- Dans l'explorateur Windows, sélectionner un ou plusieurs fichiers
- "Tirer" les fichiers sur l'exécutable du client pour lancer ce dernier avec les noms de fichiers en paramètres
Le serveur doit afficher les chemins des fichiers sélectionnés, et le client afficher "Bien reçu".

Code du client:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Linq;

public class Client
{
    public static void Main()
    {
        // Récupération des arguments de la ligne de commande après suppression du chemin de l'exécutable
        string[] Args = Environment.GetCommandLineArgs();
        Args = Args.Skip(1).ToArray();
        string FileList = string.Join("|", Args);

        // Création et enregistrement du canal IPC
        IpcClientChannel clientChannel = new IpcClientChannel();
        ChannelServices.RegisterChannel(clientChannel, true);

        // Enregistrement de l'objet d'échange
        RemotingConfiguration.RegisterWellKnownClientType(typeof(LoadFile), "ipc://SudokuHelper/LoadFile");

        // Création d'un objet de la classe LoadFile => connexion au serveur
        LoadFile LoadFile = new LoadFile();
        LoadFile.Files2Load = FileList;             // Envoi du message au serveur
        Console.WriteLine(LoadFile.Files2Load);     // Le retour du serveur
        Console.ReadLine();                         // Pour pouvoir voir le retour du serveur

        // L'appli se ferme ici.
    }
}


Code du serveur :
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;

public class IpcServer
{

    public static void Main()
    {
        // Création et enregistrement de canal IPC
        IpcServerChannel serverChannel = new IpcServerChannel("SudokuHelper");
        ChannelServices.RegisterChannel(serverChannel, true);

        // Enregistrement de l'objet d'échange
        RemotingConfiguration.RegisterWellKnownServiceType(typeof(LoadFile), "LoadFile", WellKnownObjectMode.Singleton);

        // Attente des appels
        Console.WriteLine("Ecoute du canal {0}", serverChannel.GetChannelUri());
        Console.ReadLine();
    }
}


L'objet de communication :
public class LoadFile : MarshalByRefObject
{
    public string Files2Load
    {
        set { Console.WriteLine(value.Replace("|", "\n")); }
        get { return "Bien reçu"; }
    }
}


À suivre...
0
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 2
30 juil. 2019 à 12:05
J'ai oublié de préciser : si le using System.Runtime.Remoting.Channels.Ipc n'est pas reconnu (cas par exemple du .net 4.0), il faut dans les deux projets ajouter aux références la dll System.Runtime.Remoting.dll
0
MGD Software Messages postés 186 Date d'inscription vendredi 1 septembre 2006 Statut Membre Dernière intervention 23 avril 2022 2
Modifié le 30 juil. 2019 à 14:05
Youpi ! Ça fonctionne.

Pour ceux qui voudraient faire pareil, je mets la partie concernée du code de mon appli :
(Pour les using, voir le code de la maquette ci-dessus)

Dans l'évènement Load de la feuille MDI :
// Double clic sur un ou plusieurs fichiers, drag-drop ou 'ouvrir' dans l'explorateur => noms des fichiers dans la ligne de commande (tableau, avec [0] = chemin de l'exécutable)
string[] CmdArgs = Environment.GetCommandLineArgs();        

Process aProcess = Process.GetCurrentProcess();
string aProcName = aProcess.ProcessName;
Process[] Processes = Process.GetProcessesByName(aProcName);

if (Processes.Length > 1)       // Instance secondaire : installation du IpcClient
{
    this.Visible = false;

    // Récupération des arguments de la ligne de commande après suppression du chemin de l'exécutable
    string[] Args = CmdArgs.Skip(1).ToArray();
    string FileList = string.Join("|", Args);

    // Création et enregistrement du canal IPC
    IpcClientChannel clientChannel = new IpcClientChannel();
    ChannelServices.RegisterChannel(clientChannel, true);

    // Enregistrement de l'objet d'échange
    RemotingConfiguration.RegisterWellKnownClientType(typeof(LoadFile), "ipc://SudokuHelper/LoadFile");

    // Création d'un objet de la classe LoadFile => connexion au serveur
    // Si le serveur est fermé pendant la communication, on a une exception qu'on ne traite pas
    // En effet, cela ne peut guère se produire que si on met un point d'arrêt dans l'objet de comm du serveur
    try
    {
        LoadFile LoadFile = new LoadFile();
        LoadFile.Files2Load = FileList;             // Envoi du message au serveur
        Console.WriteLine(LoadFile.Files2Load);     // Le retour du serveur
    }
    finally
    {
        Application.ExitThread();
    }

}
else            // Instance principale : installation d'un IpcServer
{
    // Création et enregistrement de canal IPC
    IpcServerChannel ServerChannel = new IpcServerChannel("SudokuHelper");

    ChannelServices.RegisterChannel(ServerChannel, true);

    // Enregistrement de l'objet d'échange
    RemotingConfiguration.RegisterWellKnownServiceType(typeof(LoadFile), "LoadFile", WellKnownObjectMode.Singleton);

    // Attente des appels
    Console.WriteLine("Listening on {0}", ServerChannel.GetChannelUri());
}


et la classe de l'objet de communication :
public class LoadFile : MarshalByRefObject
{
    private wMain MainForm = null;
    private int FileCount = 0;

    // A la création de l'objet, on recherche la feuille MDI maître
    public LoadFile()
    {
        foreach (Form Frm in Application.OpenForms)
            if (Frm.IsMdiContainer)
                MainForm = Frm as wMain;
    }

    public string Files2Load
    {
        // Le set envoie une liste de fichiers séparéa par le caractère '|'
        set
        {
            if (MainForm == null)   // Normalement pas possible, mais...
                FileCount = -1;
            else
            {
                // L'objet de comm est appelé par un autre thread (autre instance) => invoke pour accéder à la variable value
                MainForm.Invoke(new MethodInvoker(delegate
                {
                    string[] FileList = value.Split('|');
                    FileCount = FileList.Length;
                    foreach (string FilePath in FileList)
                        MainForm.OnMruFile(0, FilePath);        // Chargement du fichier (menu public)
                    if (MainForm.WindowState == FormWindowState.Minimized)
                        MainForm.WindowState = FormWindowState.Normal;      // Restauration de la fenêtre
                    //MainForm.BringToFront();        // Passage au premier plan - Ne marhe pas
                    MainForm.TopMost = true;        // Passage au premier plan - Fonctionne
                    MainForm.TopMost = false;       // Pour ne pas rester toujours au-dessus
                }));
            }
        }

        // Le get est supposé être appelé juste après le set pour gérer éventuellement le nombre de fichiers
        get
        {
            if (MainForm == null)
                return "Feuille MDI inconnue";
            else
            {
                // On retourne le nombre de fichiers envoyés
                string Reponse = FileCount.ToString();
                FileCount = 0;     // Remise à zéro du compteur
                return Reponse;
            }
        }
    }
}


A ma grande surprise, l'objet LoadFile, créé en local dans la procédure Load de la feuille principale, reste 'vivant' et reçoit les appels des clients toute la durée de l'exécution du programme - ce qui m'arrange fortement!
Donc pas besoin de thread explicite, encore que la réception des messages nécessite pour être traitée dans la feuille principale d'utiliser un Invoke.

Cette fois, mon appli est terminée (sauf bugs)
http://mgd.software.free.fr/downloads/Freewares/SudokuHelper/
0
Rejoignez-nous