Déboguer un contexte exécutable sous le modèle COM

Avertissement : ce document porte sur des détails spécifiques au modèle COM tel qu'implémenté sous Microsoft Windows, et se veut principalement un support aux cours de Systèmes client/ serveur (SCS) du Collège Lionel-Groulx et de l'Université de Sherbrooke. L'activité proposée repose sur les concepts exposés et les outils explorés et développés dans l'un ou l'autre de ces cours. Si vous ne suivez pas l'un de ces cours et si vous n'avez pas les connaissances préalables à l'exercice qui suit, il est probable que vous n'en tiriez pas profit.

Il est aussi présumé que la lectrice/ le lecteur de ce document est familère/ familier avec un débogueur, probablement celui de Visual Studio.

Ce document prend la forme d'un semi pas-à-pas guidant l'étudiant(e) dans le processus de reconnaître une erreur dans un serveur à contexte externe; de tracer l'exécution de ce serveur à l'aide d'un débogueur; d'identifier cette erreur; puis de la corriger.

Le travail qui suit repose sur le SCS constitué des quatre projets insérés dans l'archive ZIP que vous obtiendrez en sélectionnant la cible à gauche de ce paragraphe. Vous aurez besoin de Visual Studio 2008 pour générer ces projets (une version plus récente devrait aussi fonctionner, mais l'éditeur procédera alors à une conversion du projet).

Il vous faudra incrire au registre la DLL de marshalling et le contexte externe avant de pouvoir procéder à l'activité détaillée ci-dessous.

Il est normal que l'exécution du client soit déficiente, ceci à cause du problème résidant dans le serveur: l'idée de l'activité est de montrer comment identifier et corriger des erreurs dans un contexte externe!

Il arrive, avec le modèle COM comme avec d'autres modèles CS par composants, qu'on veuille déboguer un serveur résidant dans un contexte externe, donc un composant résidant dans un exécutable distinct de celui dans lequel réside son client, par opposition à un serveur résidant dans un contexte interne – une bibliothèque à liens dynamiques (DLL) ou un Shared Object (.so), selon les plateformes – et se trouvant donc logé dans l'espace adressable de son client.

Ainsi, imaginons le client ci-dessous, ayant pour objectif de calculer la somme de 2 et de 3 à l'aide des sophistiqués services d'un serveur conçu précisément pour acomplir cette tâche :

#define _WIN32_DCOM // assurer la présence des déclarations propres à COM
#include "../../Commun/Commun/Somme_h.h" // déclaration C++ de ISomme
#include "../../Commun/Commun/UUID_Fabriques.h" // UUID de la fabrique
#include "ChargeurCOM.h"
#include <iostream>
int main()
{
   using namespace std::cout;
   ChargeurCOM chcom{ChargeurCOM::MultiThreaded{}};
   const int NBITF_VOULUES = 1,
             ITF_ISOMME    = 0;
   MULTI_QI ItfDemandees[NBITF_VOULUES] = { { &IID_ISomme,  0, S_OK } };
   COSERVERINFO Serveur =
   {
      0,  L"localhost", 0, 0
   };
   HRESULT hr = CoCreateInstanceEx
                   (CLSID_FabriqueSommeExterne, // fabrique voulue
                    0, // != 0 seulement pour un agrégat
                    CLSCTX_SERVER, // contexte préféré de serveur
                    &Serveur,      // le COSERVERINFO
                    NBITF_VOULUES, // nb. éléments du tableau ItfDemandees
                    ItfDemandees); // MULTI_QI[]
   if (SUCCEEDED(hr))
   {
      if (SUCCEEDED(ItfDemandees[ITF_ISOMME].hr))
      {
         ISomme *pSomme = static_cast<ISomme*>(ItfDemandees[ITF_ISOMME].pItf);
         int resultat;
         hr = pSomme->Somme(2, 3, &resultat);
         if (SUCCEEDED(hr))
            cout << "Somme de 2 et de 3 (via ISomme) : "
                 << resultat << endl;
         pSomme->Release();
      }
   }
}

Si vous exécutez ce client tel quel, vous ferez face à un écran console noir (ou, si vous exécutez le tout dans l'environnement de Visual Studio, au sympathique message Pressez une touche pour continuer).

// ... lui?
if (SUCCEEDED(hr))
{
   // ... ou lui?
   if (SUCCEEDED(ItfDemandees[ITF_ISOMME].hr))
   {
      // ...

Ceci indique que l'un des deux if (SUCCEEDED(...)) a reconnu un problème – en effet, dans le cas contraire, le flot d'exécution du programme nous aurait amené à l'intérieur de la seconde alternative, aurait réalisé l'imposant calcul et en aurait fièrement affiché le résultat.

De deux choses l'une : ou la co-création du serveur a échoué, ou elle a réussi mais la tentative d'obtention de l'interface ISomme a échoué. Pour poser un diagnostic, il nous faudra examiner la valeur du HRESULT obtenu dans chaque cas.

Si vous obtenez un autre message, vous avez probablement négligé une étape d'enregistrement du serveur.

Prenez soin d'exécuter le contexte avec paramètre -RegServer et d'inscrire la DLL de marshalling au registre avant de poursuivre!

Une trace pas à pas du client nous fait constater que le HRESULT contenu dans la variable hr, et donc retourné par l'appel à CoCreateInstanceEx(), signifie E_NOINTERFACE.

Ce message peut signifier que nous avons négligé d'inscrire la DLL de marshalling au registre, ou que notre fabrique expose un QueryInterface() déficient, ou encore que notre composant expose un QueryInterface() déficient.

Supposons que nous n'ayons pas vraiment d'idée du lieu où, dans le contexte du serveur, réside le problème que nous cherchons à diagnostiquer. Nous aimerions bien tracer le contexte en question, or celui-ci est chargé en mémoire puis décède aussitôt lorsque nous lançons le client.

De plus, c'est COM qui charge l'exécutable dans lequel réside notre serveur, pas nous. Nous ne pouvons pas lancer ce programme manuellement, du moins pas si nous désirons faire un suivi rigoureux de la mécanique du contexte et tirer un diagnostic plausible.

La stratégie

S'attacher à un processus

Avec Visual Studio, la manière de procéder pour déboguer un processus en cours d'exécution est simple: dans le menu Outils, choisissez Déboguer les processus, puis sélectionnez le processus auquel vous désirez vous attacher (évidemment, pour nous, ce sera l'exécutable servant de contexte au serveur).

Ensuite, suffit d'ouvrir les fichiers sources qui vous intéressent, de placer des points d'arrêts (Breakpoints) aux endroits qui vous intéressent, puis de pousuivre l'exécution du processus en débogage jusqu'à ce que l'un de ces points d'arrêts soit rencontré.

On peut déboguer un processus en cours d'exécution, dans la mesure où certains critères sont rencontrés :

En effet, la clé d'une bonne procédure de test comme d'une bonne stratégie de débogage est de planifier avant d'agir. Avoir une hypothèse de travail, ne pas chercher partout et nulle part à la fois.

Nous avons de telles hypothèses, suggérées par le HRESULT obtenu à l'appel de CoCreateInstance(). Par contre, si nous lançons le client, qui lancera le contexte du serveur de par son appel à CoCreateInstance(), tout se passera trop rapidement pour que nous puissions être en mesure de nous attacher au contexte du serveur et pour que nous puissions le tracer.

Est-ce correct?

Modifier temporairement les sources du contexte du serveur pour accomoder notre opération de débogage est tout à fait convenable. Nous travaillons sur un produit à livrer (ou sur la correction de bugs dans un produit déjà livré), pas sur la version commercialisée du produit.

Il nous faudra nous assurer que le processus servant de contexte au serveur soit bloqué jusqu'à ce que nous nous soyons attachés à lui et jusqu'à ce que nous ayons pu entreprendre le processus de débogage.

Nous allons donc insérer une opération bloquante dans le contexte du serveur à un moment qui sera suffisamment tôt dans l'exécution du processus pour que nous puissions prendre le relais et tracer le code qui nous intéresse, et suffisamment tard pour ne pas que nous perdions inutilement notre temps.

Plusieurs stratégies sont possibles pour y arriver, de la fenêtre modale (p. ex. : MessageBox()) à la mise en attente en réception sur un socket à la lecture toute simple au clavier. Dans la mesure où vous êtes capable d'interagir directement avec le contexte du serveur (et ce ne sera pas toujours le cas, surtout si le serveur est sur un poste distant de celui du client, alors prudence!), une lecture au clavier suffira; sinon, un socket (ou l'équivalent) pourra aider.

Je vous suggère donc de modifier (temporairement) le code source du serveur pour qu'il passe de la version proposée à gauche à celle proposée à droite. Cette modification doit être temporaire et pour débogage seulement – le code tel que modifié est inacceptable pour du code de production.

Avant Après
int main(int argc, char *argv[])
{
   using namespace std;
   if (argc == 2)
   {
      const string
         PREFIXE_WIN = "/", PREFIXE_UNIX = "-",
         PARAM = Majuscules(argv[1]),
         ENREGISTRER = Majuscules ("RegServer"),
         DESENREGISTRER = Majuscules ("UnregServer"),
         LANCEMENTCOM= Majuscules ("Embedding");
      if (PARAM == PREFIXE_WIN  + ENREGISTRER ||
          PARAM == PREFIXE_UNIX + ENREGISTRER)
      {
         // inscription au registre (omis par économie)
      }
      else if (PARAM == PREFIXE_WIN  + DESENREGISTRER ||
               PARAM == PREFIXE_UNIX + DESENREGISTRER)
      {
         // suppression du registre (omis par économie)
      }
      else if (PARAM == PREFIXE_WIN  + LANCEMENTCOM ||
               PARAM == PREFIXE_UNIX + LANCEMENTCOM)
      {
         // agir comme un serveur COM
         ChargeurCOM ccom{ChargeurCOM::MultiThreaded{}};
         LancerContexte();
         GestionnaireDeVerrou::get()->attendre_fin();
         ArreterContexte();
      }
      else
      {
         // traitement d'erreur (omis par économie)
      }
   }
   else
   {
      // traitement d'erreur (omis par économie)
   }
}
int main(int argc, char *argv[])
{
   using namespace std;
   if (argc == 2)
   {
      const string
         PREFIXE_WIN = "/", PREFIXE_UNIX = "-",
         PARAM = Majuscules(argv[1]),
         ENREGISTRER = Majuscules ("RegServer"),
         DESENREGISTRER = Majuscules ("UnregServer"),
         LANCEMENTCOM= Majuscules ("Embedding");
      if (PARAM == PREFIXE_WIN  + ENREGISTRER ||
          PARAM == PREFIXE_UNIX + ENREGISTRER)
      {
         // inscription au registre (omis par économie)
      }
      else if (PARAM == PREFIXE_WIN  + DESENREGISTRER ||
               PARAM == PREFIXE_UNIX + DESENREGISTRER)
      {
         // suppression du registre (omis par économie)
      }
      else if (PARAM == PREFIXE_WIN  + LANCEMENTCOM ||
               PARAM == PREFIXE_UNIX + LANCEMENTCOM)
      {
         // agir comme un serveur COM
         cout << "Debug -- Pressez une touche...";
         char c;
         cin >> c;
         ChargeurCOM ccom{ChargeurCOM::MultiThreaded{}};
         LancerContexte();
         GestionnaireDeVerrou::get()->attendre_fin();
         ArreterContexte();
      }
      else
      {
         // traitement d'erreur (omis par économie)
      }
   }
   else
   {
      // traitement d'erreur (omis par économie)
   }
}

Ce qu'il faut comprendre ici est que si l'opération bloquante insérée dans le code est atteinte, alors le contexte a été chargé par COM et il est donc pertinent de le tracer. La position dans le code où l'opération bloquante a été insérée n'est pas anodine: la mécanique de COM intervient à partir de la fonction LancerContexte(), et c'est à partir de ce moment seulement qu'il devient intéressant d'investiguer plus à fond.

L'opération bloquante sera atteinte si COM charge le contexte sur demande d'un client. Cela nous permettra de nous brancher sur le processus servant de contexte au serveur avec un débogueur, puis d'insérer un point d'arrêt sur les lignes nous semblant pertinentes et de laisser l'exécution se poursuivre en sélectionnant la fenêtre console du contexte en questiion puis appuyant sur une touche suivie de <return>.

Prudence : si l'opération bloquante est atteinte, c'est qu'un client est lui-même bloqué en attente de la complétion de son appel à CoCreateInstance(). Cette opération se terminera par un timeout après écoulement d'un certain délai, alors si vous désirez vous brancher au contexte, il importe de se dépêcher (rien d'inhumain, on s'entend, mais ce n'est pas le meilleur moment pour aller se chercher un café).

Une stratégie alternative, suggérée par Francis Moreau de la cohorte 01 du DGL, va comme suit :

Avant Après
int main(int argc, char *argv[])
{
   using namespace std;
   if (argc == 2)
   {
      const string
         PREFIXE_WIN = "/", PREFIXE_UNIX = "-",
         PARAM = Majuscules(argv[1]),
         ENREGISTRER = Majuscules ("RegServer"),
         DESENREGISTRER = Majuscules ("UnregServer"),
         LANCEMENTCOM= Majuscules ("Embedding");
      if (PARAM == PREFIXE_WIN  + ENREGISTRER ||
          PARAM == PREFIXE_UNIX + ENREGISTRER)
      {
         // inscription au registre (omis par économie)
      }
      else if (PARAM == PREFIXE_WIN  + DESENREGISTRER ||
               PARAM == PREFIXE_UNIX + DESENREGISTRER)
      {
         // suppression du registre (omis par économie)
      }
      else if (PARAM == PREFIXE_WIN  + LANCEMENTCOM ||
               PARAM == PREFIXE_UNIX + LANCEMENTCOM)
      {
         // agir comme un serveur COM
         ChargeurCOM ccom{ChargeurCOM::MultiThreaded{}};
         LancerContexte();
         GestionnaireDeVerrou::get()->attendre_fin();
         ArreterContexte();
      }
      else
      {
         // traitement d'erreur (omis par économie)
      }
   }
   else
   {
      // traitement d'erreur (omis par économie)
   }
}
int main(int argc, char *argv[])
{
   using namespace std;
   if (argc == 2)
   {
      const string
         PREFIXE_WIN = "/", PREFIXE_UNIX = "-",
         PARAM = Majuscules(argv[1]),
         ENREGISTRER = Majuscules ("RegServer"),
         DESENREGISTRER = Majuscules ("UnregServer"),
         LANCEMENTCOM= Majuscules ("Embedding");
      if (PARAM == PREFIXE_WIN  + ENREGISTRER ||
          PARAM == PREFIXE_UNIX + ENREGISTRER)
      {
         // inscription au registre (omis par économie)
      }
      else if (PARAM == PREFIXE_WIN  + DESENREGISTRER ||
               PARAM == PREFIXE_UNIX + DESENREGISTRER)
      {
         // suppression du registre (omis par économie)
      }
      else if (PARAM == PREFIXE_WIN  + LANCEMENTCOM ||
               PARAM == PREFIXE_UNIX + LANCEMENTCOM)
      {
         // agir comme un serveur COM
         _asm int 3;
         ChargeurCOM ccom{ChargeurCOM::MultiThreaded{}};
         LancerContexte();
         GestionnaireDeVerrou::get()->attendre_fin();
         ArreterContexte();
      }
      else
      {
         // traitement d'erreur (omis par économie)
      }
   }
   else
   {
      // traitement d'erreur (omis par économie)
   }
}

Cette stratégie a l'avantage de générer, à bas niveau, une erreur d'exécution qui provoquera le lancement du débogueur sans demander d'intervention à la console de la part d'un utilisateur, ce qui explique qu'elle fonctionnera même en l'absence de fenêtre pour interagir (comme dans le cas où le contexte du serveur est un service ou encore s'il est configuré, comme c'est souvent le cas, de manière telle que son exécution se fasse en arrière-plan).

Une fois attaché au processus, nous voudrons placer des points d'arrêts aux endroits identifiés lors de notre hypothèse initiale, ce qui signifie dans ce cas-ci :

C'est tout?

Une fois branché au contexte, le problème en devient un de débogage normal.

Il est clair que QueryInterface() n'est pas le seul lieu dans un contexte COM où des problèmes peuvent survenir; faites vos hypothèses a priori et investiguez!

Ne reste plus qu'à laisser le tout fonctionner et à tracer normalement le code. Examinez les questions posées à vos composants (les REFIID passés en paramètre à QueryInterface()) et les réponses offertes en retour par ces composants.

Vérifiez que les bonnes réponses sont données dans chaque cas, et que le comptage de références respecte les règles du modèle. Vous devriez arriver à trouver le problème par vous-mêmes – et à le corriger.


Valid XHTML 1.0 Transitional

CSS Valide !