Translate

jeudi 20 décembre 2012

[TECH]: Utiliser l'objet COM+ Internet Explorer sur Windows Server 2012

Avec Exchange et Lync, il est souvent sympa de pouvoir générer des rapports divers et variés via un document HTML riche et bien formaté (au delà donc du simple usage des sorties formatées en HTML standard). Un exemple est un script que je fournis à mes clients et qui génère un rapport de santé d'un DAG et des composants d'infrastructure liés. Un exemple ci-dessous avec mon petit DAG Exchange 2013...


Le principe est de créer un objet COM Internet Explorer et de peupler un Document vide. Cela offre l'avantage de pouvoir utiliser les méthodes standards de l'objet comme getElementById() et d'aller modifier un style sur un code HTML généré en amont, par exemple ici le statut global qui passerait rouge en cas d'erreur. De plus, cela permet d'ouvrir une sorte de console IE en plain écran, de faire des rafraîchissements automatiques, ou même de sauvegarder le document complet au format HTML pour d'autres actions, type envoi par E-mail ou mise à disposition sur une racine IIS.

$IE = New-Object -comObject InternetExplorer.Application
$IE.Navigate2("about:blank")

A partir de là, la logique est d'attendre la fin du statut occupé de l'objet IE, puis de travailler avec l'attribut membre "Document", en utilisant les méthodes write() et writeln() afin de générer le code HTML.

Lorsque j'ai commencé à mettre à jour mon script de rapport de santé pour le rendre compatible Exchange 2013, je me suis retrouvé face à un problème de taille: mes serveurs Exchange 2013 étant installés sur un système d'exploitation Windows Server 2012, impossible d'utiliser les méthodes write() et writeln(). Elles existent toujours mais ne prennent aucun argument. Rien n'y faisait, impossible de générer le code.

Puis j'ai regardé le comportement sur un autre serveur Windows 2012, mis à jour lui depuis un Windows Server 2008 R2. Et là: les méthodes existent mais les arguments à passer sont dans un type bizarre issu du code en C/C++ de vieux bouts de code IE.

On a donc une différence fonctionnelle entre:

  • Windows Server 2008 R2
  • Windows Server 2012 avec des composants issus du système originel mis à jour
  • Windows Server 2012 installé en partant de rien
J'ai donc commencé à m'intéresser à l'objet "Document", comprendre voir quelle est la nature des différences. J'ai donc regardé le type sur les trois environnements:
$IE.Document.GetType().ToString()
et
$IE |fl Document

Et là ! J'ai enfin été mis sur une piste sérieuse: les objets sont de type de base "ComObject" (des objets COM+ donc). Sur les environnements où les méthodes on bien un prototype définissant des arguments, l'objet est de type "mshtml.HTMLDocumentClass". Sur l'environnement Windows Server 2012 installé en partant de rien, l'objet apparaît être d'un type "System.__ComObject" et donc chargé sans support des méthodes MSHTML.

Après quelques recherches, j'ai fini par trouver que cette classe est définie dans une "Primary Interop Assembly" nommée "Microsoft.mshtml" et définie dans la DLL "Microsoft.mshtml.dll". Une recherche rapide sur mes serveurs à permis de confirmer que cette assembly est bien installée sur les serveurs qui me fournissent un objet définissant les méthodes write() et writeln().

Le hic c'est que cette assembly n'est pas installée en standard/par défaut. Puisqu'il semblerait qu'IE sache l'utiliser lorsqu'elle est installée, je me suis dit qu'un chargement de l'assembly à la volée pourrait faire l'affaire.

Me voici donc à développer une fonction PowerShell pour le faire, appelée naturellement "Load-Assembly". Le code est disponible ici sous forme d'un script PowerShell: http://sdrv.ms/U6j7Wn.


J'ai ensuite copié la DLL de l'assembly depuis mon serveur Windows Server 2008 R2 vers l'emplacement réseau de mes scripts, dans un sous-répertoire "bin\PrimaryInteropAssemblies".

Avant la création de mon objet Internet Explorer, j'ai donc ajouté le code suivant:

if ( ("mshtml.HTMLDocumentClass" -as [Type]) -eq $null )
{
    $assembly = Load-Assembly -Path ".\bin\PrimaryInteropAssemblies\Microsoft.mshtml.dll"
    if ( $assembly -eq $null )
    {
        throw "Failed to load the MSHTML Primary Interoperability Assembly."
        return
    }
    Write-Verbose "PrimaryInteropAssembly MSHTML loaded: $($assembly.FullName)"
}


Et enfin, une fois mon assembly chargée, le type étant défini correctement, l'objet Document redevient un objet de la classe attendue, à savoir "mshtml.HTMLDocumentClass". Une fois l'assembly chargée, mes objets COM réagissent de la même manière sur mon serveur Windows 2012 qui ne fonctionnait pas auparavant.

Me reste maintenant un problème à élucider: les méthodes write() et writeln() se refusent à fonctionner correctement malgré l'objet chargé avec la bonne classe. Faute probablement ici au PowerShell v3 je pense, qui gère les types ou la conversion de type différemment. Le type attendu est un SAFEARRAY (cf la capture d'écran en PowerShell v2) qui est un type C/C++. Imaginez la galère en partant de PowerShell...

Et là miracle: en explorant l'objet Document, je me suis rendu compte qu'il existe des méthodes additionelles à celles disponibles en PowerShell v2... bizarre mais elles sont nommées IHTMLDocument2_xxx(), IHTMLDocument3_xxx(), IHTMLDocument4_xxx() et IHTMLDocument5_xxx(). Cela ressemble fort à des méthodes de compatibilité descendante !!!

J'essaye donc IHTMLDocument2_write() et IHTMLDocument2_writeln() avec succès !!!

Au final:
  • rajout d'un bout de code permettant de charger l'assembly,
  • utilisation des méthodes de compatibilité à la place des méthodes natives... dans mon cas j'ai codé une "surobjet" Document qui s'adapte et appelle la bonne méthode en fonction de la présence ou non de la méthode IHTMLDocument2_write().
Le bug avait été remonté via Connect, ici: http://connect.microsoft.com/PowerShell/feedback/details/764756/powershell-v3-internetexplorer-application-issue

Aucun commentaire:

Enregistrer un commentaire

Enregistrer un commentaire