From 6db6e6012169fc96c131beb8dbe2cfc8a2b14f5c Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Tue, 28 Jul 2026 09:29:57 +0200 Subject: [PATCH] Fix BizTalk catalog collection and diagnostics --- Dokumentation.md | 131 ++++- Readme.md | 72 ++- .../Collectors/BizTalkWmiCollector.cs | 555 ++++++++++++++---- .../Infrastructure/ConsoleFileLogger.cs | 51 ++ .../Infrastructure/SafeCollector.cs | 6 +- src/BizTalkApplicationCatalog/Program.cs | 13 +- .../Reporting/XlsxReportWriter.cs | 3 +- 7 files changed, 683 insertions(+), 148 deletions(-) diff --git a/Dokumentation.md b/Dokumentation.md index d047ecc..a4350bd 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -21,7 +21,7 @@ ACC und PROD besitzen jeweils einen lokalen BizTalk Server 2020. Ein Prozess auf ```text ACC BizTalk Server PROD BizTalk Server | | - | lokales WMI | lokales WMI + | WMI + ExplorerOM | WMI + ExplorerOM v v ACC JSON + lokale XLSX PROD JSON + lokale XLSX \ / @@ -42,10 +42,9 @@ CommandLineOptions +-- validiert lokalen Modus oder Merge-Modus BizTalkWmiCollector - +-- MSBTS_Application - +-- MSBTS_ReceivePort - +-- MSBTS_SendPort - +-- MSBTS_ReceiveLocation + +-- lokales MSBTS_GroupSetting per WMI + +-- lädt Microsoft.BizTalk.ExplorerOM aus GAC/Installationspfad + +-- Anwendungen und Ports per BtsCatalogExplorer SnapshotStore +-- normalisiert je Anwendung Adapterzählungen @@ -58,37 +57,83 @@ XlsxReportWriter +-- Titel, Filter, Freeze Pane und Summenzeile ``` -## 4. WMI-Datenmodell +## 4. Datenzugriff -### 4.1 Anwendungen +### 4.1 Ursache des Fehlers `Invalid class` -`MSBTS_Application` ist die Pflichtquelle der Anwendungsliste. Verwendet wird nur `Name` beziehungsweise `ApplicationName`. +Die ursprüngliche Implementierung führte als erste Abfrage +`SELECT * FROM MSBTS_Application` im Namespace +`root\MicrosoftBizTalkServer` aus. Das von BizTalk Server dokumentierte WMI-Schema +enthält keine Klasse `MSBTS_Application`. WMI antwortet deshalb unabhängig von +einer administrativ gestarteten Konsole mit `Invalid class`. -Die Abfrage muss mindestens eine Anwendung liefern. Andernfalls ist der lokale Lauf unvollständig. +Auch die WMI-Portklassen bilden die fachliche Anwendungshierarchie nicht so +vollständig ab, dass damit Anwendungen ohne Ports zuverlässig erfasst werden +könnten. Der Fehler wird daher nicht nur abgefangen; der ungeeignete Zugriffsweg +wurde ersetzt. -### 4.2 Receive-Endpunkte +### 4.2 Lokale Gruppenermittlung per WMI -Eine `MSBTS_ReceiveLocation` zählt als ein Endpunkt. Der Adapter wird defensiv aus `AdapterName`, `TransportType` oder `PTTransportType` gelesen. +Verwendeter Namespace: -Da eine Receive Location abhängig vom WMI-Provider keine direkte `ApplicationName` liefern kann, wird vorher `MSBTS_ReceivePort` gelesen. Über `ReceivePortName` wird die Anwendung aufgelöst. Ein Receive Port zählt selbst nicht als Endpunkt. +```text +\\.\root\MicrosoftBizTalkServer +``` -### 4.3 Send-Endpunkte +Verwendete Abfrage: -Bei `MSBTS_SendPort` zählt die primäre Transportkonfiguration als ein Endpunkt. Ist eine sekundäre Transportkonfiguration vorhanden, zählt sie als zusätzlicher Endpunkt. +```sql +SELECT Name, MgmtDbServerName, MgmtDbName FROM MSBTS_GroupSetting +``` -Primärer Adapter: +Der Punkt im Namespace erzwingt den lokalen Computer. Die Abfrage liefert den +BizTalk-Gruppennamen und das Ziel der Management-Datenbank. Der konfigurierte +WMI-Timeout wird angewendet. Kennwörter werden weder benötigt noch protokolliert. -- `PTTransportType` -- Fallback `PrimaryTransportType` -- Fallback `AdapterName` -- Fallback `TransportType` +### 4.3 Kataloglesung per Explorer Object Model -Sekundärer Adapter: +Die Assembly `Microsoft.BizTalk.ExplorerOM.dll` wird zur Laufzeit geladen. Dadurch +bleibt das Quellprojekt auf einem Build-Rechner ohne BizTalk-Assembly kompilierbar; +für den echten Inventarlauf müssen die BizTalk-Verwaltungskomponenten lokal +installiert sein. -- `STTransportType` -- Fallback `SecondaryTransportType` +Ladestrategie: -Fehlt ein gelieferter Adaptername, wird `Unbekannt` verwendet. Ein Endpunkt ohne Anwendungszuordnung macht den Lauf unvollständig, weil er sonst in der Anwendungssicht verloren ginge. +1. normale Typauflösung der bereits verfügbaren Assembly +2. Laden aus dem Global Assembly Cache +3. kompatible GAC-Auflösung ohne fest verdrahtete Assemblyversion +4. bekannte BizTalk-Installationsverzeichnisse unter `Program Files` + +`BtsCatalogExplorer.ConnectionString` verwendet ausschließlich integrierte +Windows-Authentifizierung. Die Anwendung ruft weder `SaveChanges` noch eine andere +ändernde ExplorerOM-Methode auf. + +Die Eigenschaft `Applications` liefert auch Anwendungen ohne Ports. Pro Anwendung +werden nur folgende Eigenschaften gelesen: + +- `Name` +- `SendPorts` +- `SendPort.PrimaryTransport.TransportType.Name` +- `SendPort.SecondaryTransport.TransportType.Name` +- `ReceivePorts` +- `ReceivePort.ReceiveLocations` +- `ReceiveLocation.TransportType.Name` + +### 4.4 Receive-Endpunkte + +Eine Receive Location zählt als ein Endpunkt. Der Adaptername stammt aus +`ReceiveLocation.TransportType.Name`. Ein Receive Port ist nur ein Container und +zählt selbst nicht als Endpunkt. + +### 4.5 Send-Endpunkte + +Bei einem Send Port zählt die primäre Transportkonfiguration als ein Endpunkt. Ist +eine sekundäre Transportkonfiguration vorhanden, zählt sie als zusätzlicher +Endpunkt. + +Fehlt ein Adaptername, wird `Unbekannt` verwendet. Die Anwendung ist durch die +Iteration innerhalb von `Application.SendPorts` beziehungsweise +`Application.ReceivePorts` eindeutig vorgegeben. ## 5. Snapshot-Format @@ -170,7 +215,8 @@ Der Filter umfasst nur Kopf- und Datenzeilen, nicht die Summenzeile. Die ersten ## 8. Fehlerverhalten -Alle vier WMI-Abfragen bilden einen Pflichtabschnitt. Ein Fehler führt zu: +Die WMI-Gruppenermittlung und die vollständige ExplorerOM-Kataloglesung bilden +gemeinsam einen Pflichtabschnitt. Ein Fehler führt zu: - Exitcode `1` - Logeintrag @@ -181,11 +227,33 @@ Der Merge lehnt einen solchen JSON-Snapshot ab. Dadurch werden technische Lücke Ungültige Parameter, fehlende Dateien, fehlerhaftes JSON und Schreibfehler liefern Exitcode `2`. +### 8.1 Diagnosedaten + +Fortschrittsmeldungen werden gleichzeitig auf die Konsole und in eine +UTF-8-Logdatei geschrieben. Die Detailprotokollierung umfasst: + +- Laufzeit-, Betriebssystem- und Prozessarchitektur +- Windows-Identität und Administratorstatus +- WMI-Namespace und Timeout +- vollständige WQL-Anweisung, Laufzeit und Datensatzanzahl +- BizTalk-Gruppe sowie Management-DB-Server und -Name +- ExplorerOM-Assemblyname, Version und Dateipfad +- jede gelesene Anwendung und jeden gezählten Endpunkt mit Adaptertyp +- Gesamtzahlen für Anwendungen, Send Ports, sekundäre Transporte, Receive Ports + und Receive Locations + +Eine Ausnahme wird mit Typ, Meldung, HRESULT in Hexadezimalschreibweise, +`ManagementException.ErrorCode`, inneren Ausnahmen und Stacktrace ausgegeben. +`TargetInvocationException` wird dabei bis zur eigentlichen Provider- oder +Datenbankausnahme aufgelöst. + ## 9. Sicherheit - ausschließlich lesende lokale WMI-Abfragen -- keine Änderungen an BizTalk-Konfiguration oder Runtime -- keine SQL-Abfragen +- ausschließlich lesende Katalogabfragen über das lokale BizTalk Explorer Object Model +- keine Aufrufe von `SaveChanges` oder ändernden BizTalk-Methoden +- keine selbst formulierten SQL-Abfragen; ExplorerOM liest die Management-Datenbank + über integrierte Windows-Authentifizierung - keine Endpunktadressen - keine Binding-Exporte - keine Credentials, Secrets oder Tokens @@ -203,6 +271,7 @@ Baseline: - C# 7.3 - keine NuGet-Pakete - kein .NET SDK erforderlich +- keine compile-time-Referenz auf `Microsoft.BizTalk.ExplorerOM.dll` ```cmd scripts\build-release.cmd @@ -227,10 +296,12 @@ Je Umgebung: 1. `--self-test` erfolgreich ausführen. 2. Lokalen Lauf in administrativer `cmd.exe` starten. -3. Exitcode `0` und Log prüfen. -4. Anwendungsgesamtzahl mit der BizTalk Administration Console vergleichen. -5. Send Ports, sekundäre Transporte und Receive Locations stichprobenartig zählen. -6. Nur die vollständige JSON-Datei für den Merge verwenden. +3. Im Log die erfolgreiche WMI-Abfrage `MSBTS_GroupSetting` prüfen. +4. Im Log Assemblyname, Version und Pfad von ExplorerOM prüfen. +5. Exitcode `0` und `IsComplete: true` prüfen. +6. Anwendungsgesamtzahl mit der BizTalk Administration Console vergleichen. +7. Send Ports, sekundäre Transporte und Receive Locations stichprobenartig zählen. +8. Nur die vollständige JSON-Datei für den Merge verwenden. Nach dem Merge: diff --git a/Readme.md b/Readme.md index c6ecc83..a221cce 100644 --- a/Readme.md +++ b/Readme.md @@ -26,12 +26,18 @@ Jeder lokale Lauf erzeugt zusätzlich eine einblättrige Excel-Sicht für die je ## Erfasste Daten -Das Tool liest ausschließlich: +Das Tool verwendet zwei ausschließlich lesende, lokale BizTalk-Schnittstellen: -- `MSBTS_Application` für die vollständige Anwendungsliste -- `MSBTS_SendPort` für primäre und gegebenenfalls sekundäre Send-Endpunkte -- `MSBTS_ReceivePort` nur zur Zuordnung einer Receive Location zur Anwendung -- `MSBTS_ReceiveLocation` für Receive-Endpunkte +- WMI-Klasse `MSBTS_GroupSetting` im Namespace + `root\MicrosoftBizTalkServer`, um Server und Name der BizTalk-Management-Datenbank + der lokalen BizTalk-Gruppe zu ermitteln; +- das lokal installierte `Microsoft.BizTalk.ExplorerOM`, um Anwendungen, Send Ports, + sekundäre Send-Transporte, Receive Ports und Receive Locations vollständig zu lesen. + +Das Explorer Object Model ist notwendig, weil es im BizTalk-WMI-Schema keine Klasse +`MSBTS_Application` gibt. Die frühere Abfrage dieser nicht vorhandenen Klasse führte +unmittelbar zu `Invalid class` beziehungsweise WMI-Status `InvalidClass`. Adminrechte +ändern nicht, welche Klassen ein WMI-Provider bereitstellt. Ein Endpunkt ist: @@ -48,8 +54,13 @@ Nicht erfasst werden insbesondere Adressen, Credentials, Beschreibungen, Status, - BizTalk Server 2020 auf Windows Server 2019 - lokale Ausführung auf dem jeweiligen BizTalk-Server - .NET Framework 4.7.2 oder höher +- lokal installierte BizTalk-Verwaltungskomponenten einschließlich + `Microsoft.BizTalk.ExplorerOM.dll` - Leserechte auf `root\MicrosoftBizTalkServer` -- administrative `cmd.exe` empfohlen +- Leserechte auf den BizTalk-Katalog, vorzugsweise über die konfigurierte Gruppe + `BizTalk Server Read Only Users` oder eine höher berechtigte BizTalk-Rolle +- administrative `cmd.exe` ist für die Diagnose hilfreich, ersetzt aber keine + BizTalk- beziehungsweise SQL-Leseberechtigung Microsoft Excel, Microsoft Office, PowerShell, Internetzugriff, NuGet und ein .NET SDK werden auf dem Zielserver nicht benötigt. @@ -83,6 +94,52 @@ Phase1-BizTalk-Anwendungskatalog-ACC-SERVER-20260727-150000.log Nur JSON-Dateien eines Laufs mit Exitcode `0` dürfen zusammengeführt werden. Ein unvollständiger Snapshot trägt `IsComplete: false` und wird vom Merge abgelehnt. +## Konsolen- und Dateilogging + +Jede Logmeldung wird unmittelbar gleichzeitig auf der Konsole und in der `.log`-Datei +ausgegeben. Der lokale Inventarlauf protokolliert unter anderem: + +- Computer, Betriebssystem, CLR- und Prozessarchitektur +- Windows-Sicherheitskontext und erkannte Administratorrolle +- WMI-Namespace, Timeout, WQL-Abfrage, Dauer und Datensatzanzahl +- erkannte BizTalk-Gruppe sowie Management-Datenbankziel ohne Kennwort +- geladene ExplorerOM-Assembly einschließlich Version und Pfad +- jede Anwendung sowie Send Ports, sekundäre Transporte und Receive Locations +- Adaptertyp und Summen je Erfassungsart +- bei Fehlern: Ausnahmetyp, Meldung, HRESULT, WMI-Status, innere Ausnahmen und Stacktrace + +Die Logdatei enthält interne Namen und ist wie JSON und XLSX geschützt abzulegen. + +## Fehleranalyse + +### `Invalid class` direkt nach dem Start + +Dieser Fehler wurde in der früheren Version durch +`SELECT * FROM MSBTS_Application` verursacht. `MSBTS_Application` ist keine +BizTalk-WMI-Klasse. Die aktuelle Version verwendet diese Abfrage nicht mehr. + +Im neuen Log muss stattdessen zunächst diese Abfrage erscheinen: + +```text +SELECT Name, MgmtDbServerName, MgmtDbName FROM MSBTS_GroupSetting +``` + +Tritt weiterhin `InvalidClass` auf, ist damit nicht mehr die Anwendungsklasse +gemeint. Dann ist die lokale BizTalk-WMI-Registrierung zu prüfen. Entscheidend sind +die protokollierten Felder `WQL`, `WMI-Status` und `HRESULT`. + +### ExplorerOM-Assembly fehlt + +Die BizTalk-Verwaltungskomponenten müssen auf dem ausführenden BizTalk-Server +installiert sein. Das Log zeigt alle geprüften Ladewege und die gefundene +Assemblyversion. + +### Zugriff auf den BizTalk-Katalog verweigert + +Eine administrativ gestartete `cmd.exe` allein garantiert keine Berechtigung auf den +BizTalk-Katalog. Das ausführende Konto benötigt mindestens die passenden +BizTalk-Leserechte. Nach einer Gruppenänderung ist eine neue Anmeldung erforderlich. + ## ACC und PROD zusammenführen Beide JSON-Dateien in einen gemeinsamen, geschützten Ordner kopieren und ausführen: @@ -164,4 +221,7 @@ Dekodieren: certutil -decode BizTalkApplicationCatalog-source.zip.txt BizTalkApplicationCatalog-source.zip ``` +Die `.txt`-Datei ist Base64 und enthält das vollständige Quellpaket einschließlich +aktualisierter `Readme.md` und `Dokumentation.md`. + Technische Details stehen in [Dokumentation.md](Dokumentation.md). diff --git a/src/BizTalkApplicationCatalog/Collectors/BizTalkWmiCollector.cs b/src/BizTalkApplicationCatalog/Collectors/BizTalkWmiCollector.cs index 8eb5a90..fce7b73 100644 --- a/src/BizTalkApplicationCatalog/Collectors/BizTalkWmiCollector.cs +++ b/src/BizTalkApplicationCatalog/Collectors/BizTalkWmiCollector.cs @@ -1,24 +1,33 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; using System.Management; +using System.Reflection; +using System.Security.Principal; using BizTalkApplicationCatalog.Infrastructure; using BizTalkApplicationCatalog.Models; namespace BizTalkApplicationCatalog.Collectors { /// - /// Liest nur die Daten, die für die Phase-1-Ansicht erforderlich sind. + /// Ermittelt die lokale BizTalk-Gruppe lesend über WMI und liest den + /// Anwendungskatalog anschließend über das BizTalk Explorer Object Model. /// internal sealed class BizTalkWmiCollector { + private const string WmiNamespace = @"\\.\root\MicrosoftBizTalkServer"; + private const string ExplorerAssemblyName = "Microsoft.BizTalk.ExplorerOM"; + private const string ExplorerTypeName = + "Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer"; + private readonly InventoryDocument document; private readonly ConsoleFileLogger logger; private readonly ManagementScope scope; private readonly TimeSpan timeout; - private readonly Dictionary receivePortApplications = - new Dictionary(StringComparer.OrdinalIgnoreCase); public BizTalkWmiCollector( InventoryDocument document, @@ -28,18 +37,18 @@ namespace BizTalkApplicationCatalog.Collectors this.document = document; this.logger = logger; timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds)); - scope = new ManagementScope( - @"\\" + Environment.MachineName + @"\root\MicrosoftBizTalkServer"); + scope = new ManagementScope(WmiNamespace); scope.Options.Timeout = timeout; + scope.Options.EnablePrivileges = false; + scope.Options.Impersonation = ImpersonationLevel.Impersonate; } public void Collect() { - scope.Connect(); - CollectApplications(); - CollectReceivePortMappings(); - CollectSendPortEndpoints(); - CollectReceiveLocationEndpoints(); + LogRuntimeInformation(); + ConnectWmi(); + var group = ReadBizTalkGroup(); + CollectWithExplorerObjectModel(group); document.Applications.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase)); @@ -58,108 +67,345 @@ namespace BizTalkApplicationCatalog.Collectors }); } - private void CollectApplications() + private void LogRuntimeInformation() { - foreach (var row in Query("SELECT * FROM MSBTS_Application")) + logger.Info("Diagnose: Computer=" + Environment.MachineName + + "; Betriebssystem=" + Environment.OSVersion.VersionString + + "; CLR=" + Environment.Version + + "; Prozess=" + (Environment.Is64BitProcess ? "64-Bit" : "32-Bit") + + "; Betriebssystem=" + + (Environment.Is64BitOperatingSystem ? "64-Bit" : "32-Bit") + "."); + logger.Detail("WMI-Namespace: " + WmiNamespace); + logger.Detail("WMI-Timeout: " + + timeout.TotalSeconds.ToString(CultureInfo.InvariantCulture) + + " Sekunden."); + + try { - using (row) + using (var identity = WindowsIdentity.GetCurrent()) { - var name = First(row, "Name", "ApplicationName"); - if (!string.IsNullOrWhiteSpace(name)) - { - document.Applications.Add(new ApplicationRecord { Name = name.Trim() }); - } + var principal = new WindowsPrincipal(identity); + logger.Info("Sicherheitskontext: Benutzer=" + identity.Name + + "; administrativ=" + + (principal.IsInRole(WindowsBuiltInRole.Administrator) + ? "Ja" + : "Nein") + + "."); } } - - if (document.Applications.Count == 0) + catch (Exception exception) { + // Ein Diagnosefehler darf die eigentliche Inventarisierung nicht verhindern. + logger.Warning("Sicherheitskontext konnte nicht ermittelt werden: " + + exception.Message); + } + } + + private void ConnectWmi() + { + var timer = Stopwatch.StartNew(); + logger.Info("Verbinde lokal mit dem BizTalk-WMI-Namespace."); + scope.Connect(); + timer.Stop(); + logger.Info("BizTalk-WMI-Verbindung hergestellt (" + + timer.ElapsedMilliseconds + " ms)."); + } + + private BizTalkGroupLocation ReadBizTalkGroup() + { + const string query = + "SELECT Name, MgmtDbServerName, MgmtDbName FROM MSBTS_GroupSetting"; + var rows = Query("BizTalk-Gruppenkonfiguration", query); + if (rows.Count != 1) + { + DisposeRows(rows); throw new InvalidOperationException( - "MSBTS_Application lieferte keine Anwendungen."); + "MSBTS_GroupSetting lieferte " + + rows.Count + + " Datensätze; exakt ein Datensatz wurde erwartet."); } - logger.Info(document.Applications.Count - + " BizTalk-Anwendung(en) gefunden."); + using (var row = rows[0]) + { + var groupName = RequiredWmiText(row, "Name"); + var serverName = RequiredWmiText(row, "MgmtDbServerName"); + var databaseName = RequiredWmiText(row, "MgmtDbName"); + logger.Info("BizTalk-Gruppe gefunden: " + groupName + "."); + logger.Info("Management-Datenbankziel: Server=" + + serverName + "; Datenbank=" + databaseName + "."); + return new BizTalkGroupLocation( + groupName, + serverName, + databaseName); + } } - private void CollectReceivePortMappings() + private void CollectWithExplorerObjectModel(BizTalkGroupLocation group) { - var count = 0; - foreach (var row in Query("SELECT * FROM MSBTS_ReceivePort")) + var explorerType = ResolveExplorerType(); + object explorer = null; + try { - using (row) + explorer = Activator.CreateInstance(explorerType); + logger.Info("BizTalk Explorer Object Model wird ausschließlich lesend geöffnet."); + + // Die Verbindungszeichenfolge enthält nur Zielserver, Datenbankname + // und integrierte Windows-Authentifizierung. Sie wird nicht protokolliert. + SetRequiredProperty( + explorer, + "ConnectionString", + "Data Source=" + group.DatabaseServer + + ";Initial Catalog=" + group.DatabaseName + + ";Integrated Security=SSPI;Persist Security Info=False;"); + + var timer = Stopwatch.StartNew(); + var applications = EnumerateRequiredProperty( + explorer, + "Applications", + "BizTalk-Anwendungen"); + timer.Stop(); + logger.Info(applications.Count + " BizTalk-Anwendung(en) geladen (" + + timer.ElapsedMilliseconds + " ms)."); + + if (applications.Count == 0) { - var name = First(row, "Name", "ReceivePortName"); - var application = First(row, "ApplicationName", "Application"); - if (string.IsNullOrWhiteSpace(name)) continue; - receivePortApplications[name] = application; - count++; + throw new InvalidOperationException( + "Das BizTalk Explorer Object Model lieferte keine Anwendungen."); + } + + CollectApplicationsAndEndpoints(applications); + } + finally + { + var disposable = explorer as IDisposable; + if (disposable != null) + { + disposable.Dispose(); + logger.Detail("BizTalk Explorer Object Model wurde geschlossen."); } } - logger.Info(count + " Receive-Port-Zuordnung(en) gelesen."); } - private void CollectSendPortEndpoints() + private void CollectApplicationsAndEndpoints(IList applications) { - var count = 0; - foreach (var row in Query("SELECT * FROM MSBTS_SendPort")) - { - using (row) - { - var application = First(row, "ApplicationName", "Application"); - AddEndpoint(application, First( - row, - "PTTransportType", - "PrimaryTransportType", - "AdapterName", - "TransportType")); - count++; + var sendPortCount = 0; + var secondaryTransportCount = 0; + var receivePortCount = 0; + var receiveLocationCount = 0; - var secondaryAdapter = First( - row, - "STTransportType", - "SecondaryTransportType"); - if (!string.IsNullOrWhiteSpace(secondaryAdapter)) + foreach (var application in applications) + { + var applicationName = RequiredText(application, "Name"); + document.Applications.Add(new ApplicationRecord + { + Name = applicationName + }); + logger.Detail("Lese Anwendung: " + applicationName + "."); + + var sendPorts = EnumerateRequiredProperty( + application, + "SendPorts", + "Send Ports der Anwendung " + applicationName); + foreach (var sendPort in sendPorts) + { + sendPortCount++; + var sendPortName = RequiredText(sendPort, "Name"); + var primaryTransport = OptionalProperty(sendPort, "PrimaryTransport"); + var primaryAdapter = AdapterName(primaryTransport); + AddEndpoint(applicationName, primaryAdapter); + logger.Detail("Send Port: Anwendung=" + applicationName + + "; Name=" + sendPortName + + "; Primäradapter=" + DisplayAdapter(primaryAdapter) + "."); + + var secondaryTransport = OptionalProperty(sendPort, "SecondaryTransport"); + if (secondaryTransport != null) { - AddEndpoint(application, secondaryAdapter); - count++; + var secondaryAdapter = AdapterName(secondaryTransport); + // ExplorerOM kann ein leeres TransportInfo-Objekt liefern, + // obwohl kein Backup-Transport konfiguriert ist. + if (!string.IsNullOrWhiteSpace(secondaryAdapter)) + { + AddEndpoint(applicationName, secondaryAdapter); + secondaryTransportCount++; + logger.Detail("Send Port: Anwendung=" + applicationName + + "; Name=" + sendPortName + + "; Sekundäradapter=" + secondaryAdapter + "."); + } + } + } + + var receivePorts = EnumerateRequiredProperty( + application, + "ReceivePorts", + "Receive Ports der Anwendung " + applicationName); + foreach (var receivePort in receivePorts) + { + receivePortCount++; + var receivePortName = RequiredText(receivePort, "Name"); + var receiveLocations = EnumerateRequiredProperty( + receivePort, + "ReceiveLocations", + "Receive Locations des Receive Ports " + receivePortName); + foreach (var receiveLocation in receiveLocations) + { + receiveLocationCount++; + var receiveLocationName = RequiredText(receiveLocation, "Name"); + var adapter = AdapterName( + OptionalProperty(receiveLocation, "TransportType")); + AddEndpoint(applicationName, adapter); + logger.Detail("Receive Location: Anwendung=" + applicationName + + "; Receive Port=" + receivePortName + + "; Name=" + receiveLocationName + + "; Adapter=" + DisplayAdapter(adapter) + "."); + } + } + + logger.Info("Anwendung abgeschlossen: " + applicationName + + "; Send Ports=" + sendPorts.Count + + "; Receive Ports=" + receivePorts.Count + "."); + } + + logger.Info("Katalogerfassung abgeschlossen: Anwendungen=" + + document.Applications.Count + + "; Send Ports=" + sendPortCount + + "; sekundäre Send-Transporte=" + secondaryTransportCount + + "; Receive Ports=" + receivePortCount + + "; Receive Locations=" + receiveLocationCount + + "; Endpunkte=" + document.Endpoints.Count + "."); + } + + private Type ResolveExplorerType() + { + logger.Info("Suche die lokale Assembly " + ExplorerAssemblyName + ".dll."); + var qualifiedTypeName = ExplorerTypeName + ", " + ExplorerAssemblyName; + var explorerType = Type.GetType(qualifiedTypeName, false); + if (explorerType != null) + { + LogExplorerAssembly(explorerType.Assembly); + return explorerType; + } + + Assembly assembly = null; + try + { + assembly = Assembly.Load(ExplorerAssemblyName); + } + catch (Exception exception) + { + logger.Detail("Assembly.Load fehlgeschlagen: " + + exception.GetType().Name + ": " + exception.Message); + } + + if (assembly == null) + { +#pragma warning disable 618 + // LoadWithPartialName ist hier bewusst nur ein Kompatibilitäts-Fallback, + // damit unterschiedliche BizTalk-GAC-Versionen gefunden werden. + assembly = Assembly.LoadWithPartialName(ExplorerAssemblyName); +#pragma warning restore 618 + } + + if (assembly == null) + { + assembly = LoadExplorerAssemblyFromInstallDirectory(); + } + + if (assembly == null) + { + throw new FileNotFoundException( + "Microsoft.BizTalk.ExplorerOM.dll wurde weder im GAC noch " + + "im BizTalk-Installationsverzeichnis gefunden. " + + "Die BizTalk-Verwaltungskomponenten müssen lokal installiert sein."); + } + + explorerType = assembly.GetType(ExplorerTypeName, false); + if (explorerType == null) + { + throw new TypeLoadException( + "Typ " + ExplorerTypeName + " fehlt in " + assembly.FullName + "."); + } + + LogExplorerAssembly(assembly); + return explorerType; + } + + private Assembly LoadExplorerAssemblyFromInstallDirectory() + { + var roots = new[] + { + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + }; + var folders = new[] + { + "Microsoft BizTalk Server", + "Microsoft BizTalk Server 2020" + }; + + foreach (var root in roots.Where(item => !string.IsNullOrWhiteSpace(item))) + { + foreach (var folder in folders) + { + var path = Path.Combine( + root, + folder, + ExplorerAssemblyName + ".dll"); + logger.Detail("Prüfe Assembly-Pfad: " + path); + if (!File.Exists(path)) continue; + try + { + return Assembly.LoadFrom(path); + } + catch (Exception exception) + { + logger.Detail("Assembly.LoadFrom fehlgeschlagen: " + + exception.GetType().Name + ": " + exception.Message); } } } - logger.Info(count + " Send-Endpunkt(e) gefunden."); + return null; } - private void CollectReceiveLocationEndpoints() + private void LogExplorerAssembly(Assembly assembly) { - var count = 0; - foreach (var row in Query("SELECT * FROM MSBTS_ReceiveLocation")) + logger.Info("ExplorerOM-Assembly geladen: " + assembly.FullName + "."); + try { - using (row) - { - var application = First(row, "ApplicationName", "Application"); - if (string.IsNullOrWhiteSpace(application)) - { - var receivePort = First(row, "ReceivePortName"); - receivePortApplications.TryGetValue(receivePort, out application); - } - - AddEndpoint( - application, - First(row, "AdapterName", "TransportType", "PTTransportType")); - count++; - } + logger.Detail("ExplorerOM-Assemblypfad: " + assembly.Location); + } + catch (NotSupportedException) + { + logger.Detail("ExplorerOM-Assemblypfad ist für diese Assembly nicht verfügbar."); + } + } + + private List Query(string name, string query) + { + var options = new System.Management.EnumerationOptions + { + ReturnImmediately = false, + Rewindable = false, + Timeout = timeout + }; + var timer = Stopwatch.StartNew(); + logger.Info("WMI-Abfrage startet: " + name + "."); + logger.Detail("WQL: " + query); + using (var searcher = new ManagementObjectSearcher( + scope, + new ObjectQuery(query), + options)) + { + var rows = searcher.Get().Cast().ToList(); + timer.Stop(); + logger.Info("WMI-Abfrage abgeschlossen: " + name + + "; Datensätze=" + rows.Count + + "; Dauer=" + timer.ElapsedMilliseconds + " ms."); + return rows; } - logger.Info(count + " Receive-Endpunkt(e) gefunden."); } private void AddEndpoint(string applicationName, string adapterType) { - if (string.IsNullOrWhiteSpace(applicationName)) - { - throw new InvalidOperationException( - "Ein Endpunkt konnte keiner BizTalk-Anwendung zugeordnet werden."); - } - document.Endpoints.Add(new EndpointRecord { ApplicationName = applicationName.Trim(), @@ -169,40 +415,135 @@ namespace BizTalkApplicationCatalog.Collectors }); } - private List Query(string query) + private static string AdapterName(object transportTypeOrInfo) { - var options = new EnumerationOptions - { - ReturnImmediately = false, - Rewindable = false, - Timeout = timeout - }; - using (var searcher = new ManagementObjectSearcher( - scope, - new ObjectQuery(query), - options)) - { - return searcher.Get().Cast().ToList(); - } + if (transportTypeOrInfo == null) return string.Empty; + + // Send-Transporte liefern zuerst TransportInfo und darin TransportType. + // Receive Locations liefern direkt den ProtocolType. + var protocolType = OptionalProperty( + transportTypeOrInfo, + "TransportType") ?? transportTypeOrInfo; + return Convert.ToString( + OptionalProperty(protocolType, "Name"), + CultureInfo.InvariantCulture) ?? string.Empty; } - private static string First(ManagementBaseObject row, params string[] names) + private static string DisplayAdapter(string adapter) { - foreach (var name in names) + return string.IsNullOrWhiteSpace(adapter) ? "Unbekannt" : adapter; + } + + private static IList EnumerateRequiredProperty( + object instance, + string propertyName, + string description) + { + var value = RequiredProperty(instance, propertyName); + var enumerable = value as IEnumerable; + if (enumerable == null) { - try - { - var value = row[name]; - if (value == null) continue; - var text = Convert.ToString(value, CultureInfo.InvariantCulture); - if (!string.IsNullOrWhiteSpace(text)) return text; - } - catch (ManagementException) - { - // WMI-Properties unterscheiden sich zwischen Providerständen. - } + throw new InvalidOperationException( + description + " ist keine aufzählbare Sammlung."); } - return string.Empty; + + var result = new List(); + foreach (var item in enumerable) + { + if (item != null) result.Add(item); + } + return result; + } + + private static string RequiredText(object instance, string propertyName) + { + var value = Convert.ToString( + RequiredProperty(instance, propertyName), + CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException( + instance.GetType().FullName + "." + propertyName + + " ist leer."); + } + return value.Trim(); + } + + private static string RequiredWmiText( + ManagementBaseObject instance, + string propertyName) + { + var value = Convert.ToString( + instance[propertyName], + CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException( + "WMI-Eigenschaft " + propertyName + " ist leer."); + } + return value.Trim(); + } + + private static object RequiredProperty(object instance, string propertyName) + { + var property = instance.GetType().GetProperty( + propertyName, + BindingFlags.Instance | BindingFlags.Public); + if (property == null) + { + throw new MissingMemberException( + instance.GetType().FullName, + propertyName); + } + return property.GetValue(instance, null); + } + + private static object OptionalProperty(object instance, string propertyName) + { + if (instance == null) return null; + var property = instance.GetType().GetProperty( + propertyName, + BindingFlags.Instance | BindingFlags.Public); + return property == null ? null : property.GetValue(instance, null); + } + + private static void SetRequiredProperty( + object instance, + string propertyName, + object value) + { + var property = instance.GetType().GetProperty( + propertyName, + BindingFlags.Instance | BindingFlags.Public); + if (property == null || !property.CanWrite) + { + throw new MissingMemberException( + instance.GetType().FullName, + propertyName); + } + property.SetValue(instance, value, null); + } + + private static void DisposeRows(IEnumerable rows) + { + foreach (var row in rows) row.Dispose(); + } + + private sealed class BizTalkGroupLocation + { + public BizTalkGroupLocation( + string groupName, + string databaseServer, + string databaseName) + { + GroupName = groupName; + DatabaseServer = databaseServer; + DatabaseName = databaseName; + } + + public string GroupName { get; private set; } + public string DatabaseServer { get; private set; } + public string DatabaseName { get; private set; } } } } diff --git a/src/BizTalkApplicationCatalog/Infrastructure/ConsoleFileLogger.cs b/src/BizTalkApplicationCatalog/Infrastructure/ConsoleFileLogger.cs index 92daaf2..689da55 100644 --- a/src/BizTalkApplicationCatalog/Infrastructure/ConsoleFileLogger.cs +++ b/src/BizTalkApplicationCatalog/Infrastructure/ConsoleFileLogger.cs @@ -1,6 +1,8 @@ using System; using System.Globalization; using System.IO; +using System.Management; +using System.Reflection; using System.Text; namespace BizTalkApplicationCatalog.Infrastructure @@ -19,8 +21,57 @@ namespace BizTalkApplicationCatalog.Infrastructure } public void Info(string message) { Write("INFO", message); } + public void Detail(string message) { Write("DETAIL", message); } public void Warning(string message) { Write("WARNUNG", message); } public void Error(string message) { Write("FEHLER", message); } + + /// + /// Schreibt die vollständige Ausnahmekette einschließlich HRESULT, + /// WMI-Status und Stacktrace auf Konsole und in die Logdatei. + /// + public void Exception(string context, Exception exception) + { + if (exception == null) + { + Error(context + ": Unbekannter Fehler ohne Ausnahmeobjekt."); + return; + } + + Error(context + ": " + exception.Message); + var current = exception; + var depth = 0; + while (current != null) + { + Detail(string.Format( + CultureInfo.InvariantCulture, + "Ausnahme[{0}]: Typ={1}; HRESULT=0x{2:X8}; Meldung={3}", + depth, + current.GetType().FullName, + current.HResult, + current.Message)); + + var managementException = current as ManagementException; + if (managementException != null) + { + Detail("WMI-Status: " + managementException.ErrorCode); + } + + if (!string.IsNullOrWhiteSpace(current.StackTrace)) + { + Detail("Stacktrace[" + depth + "]:" + Environment.NewLine + + current.StackTrace); + } + + // Reflection kapselt Providerfehler häufig in TargetInvocationException. + // Die innere Ausnahme wird deshalb ausdrücklich mit ausgegeben. + var targetInvocation = current as TargetInvocationException; + current = targetInvocation != null && targetInvocation.InnerException != null + ? targetInvocation.InnerException + : current.InnerException; + depth++; + } + } + public void Dispose() { writer.Dispose(); } private void Write(string level, string message) diff --git a/src/BizTalkApplicationCatalog/Infrastructure/SafeCollector.cs b/src/BizTalkApplicationCatalog/Infrastructure/SafeCollector.cs index c17cc82..4a800cd 100644 --- a/src/BizTalkApplicationCatalog/Infrastructure/SafeCollector.cs +++ b/src/BizTalkApplicationCatalog/Infrastructure/SafeCollector.cs @@ -52,9 +52,11 @@ namespace BizTalkApplicationCatalog.Infrastructure Severity = required ? "Fehler" : "Warnung", Area = name, Message = "Datenerfassung fehlgeschlagen: " + exception.Message, - RecommendedAction = "Logdatei, WMI-Provider und Leseberechtigungen prüfen." + RecommendedAction = + "Logdatei, WMI-Provider, ExplorerOM-Installation " + + "und BizTalk-Leseberechtigungen prüfen." }); - logger.Error(name + ": " + exception.Message); + logger.Exception(name, exception); } } } diff --git a/src/BizTalkApplicationCatalog/Program.cs b/src/BizTalkApplicationCatalog/Program.cs index 7752023..add93cd 100644 --- a/src/BizTalkApplicationCatalog/Program.cs +++ b/src/BizTalkApplicationCatalog/Program.cs @@ -73,7 +73,16 @@ namespace BizTalkApplicationCatalog logger.Info("BEW BizTalk Application Catalog – Phase 1 startet."); logger.Info("Umgebung: " + options.EnvironmentName); logger.Info("Erfasst werden nur Anwendungen, Adaptertypen und Endpunkte."); - logger.Info("Modus: ausschließlich lesender lokaler BizTalk-WMI-Zugriff."); + logger.Info( + "Modus: ausschließlich lesender Zugriff; lokales BizTalk-WMI " + + "für die Gruppenermittlung und ExplorerOM für den Katalog."); + logger.Info("Ausgabeordner: " + options.OutputDirectory); + logger.Detail("Logdatei: " + logPath); + logger.Detail("JSON-Zieldatei: " + snapshotPath); + logger.Detail("XLSX-Zieldatei: " + reportPath); + var wmiTimeoutSeconds = ReadInt("WmiTimeoutSeconds", 30); + logger.Detail("Konfiguration WmiTimeoutSeconds=" + + wmiTimeoutSeconds + "."); new SafeCollector(document, logger).Execute( "Phase-1-Anwendungen und Endpunkte", @@ -81,7 +90,7 @@ namespace BizTalkApplicationCatalog () => new BizTalkWmiCollector( document, logger, - ReadInt("WmiTimeoutSeconds", 30)).Collect()); + wmiTimeoutSeconds).Collect()); document.CompletedUtc = DateTime.UtcNow; var snapshot = SnapshotStore.FromInventory( diff --git a/src/BizTalkApplicationCatalog/Reporting/XlsxReportWriter.cs b/src/BizTalkApplicationCatalog/Reporting/XlsxReportWriter.cs index 031ad7c..5d877af 100644 --- a/src/BizTalkApplicationCatalog/Reporting/XlsxReportWriter.cs +++ b/src/BizTalkApplicationCatalog/Reporting/XlsxReportWriter.cs @@ -76,7 +76,8 @@ namespace BizTalkApplicationCatalog.Reporting { snapshots.Count > 1 ? "Phase 1 – BizTalk Anwendungen & Adapter (Quelle: ACC/PROD JSON-Snapshots)" - : "Phase 1 – BizTalk Anwendungen & Adapter (Quelle: lokaler BizTalk-WMI-Provider)", + : "Phase 1 – BizTalk Anwendungen & Adapter " + + "(Quelle: lokale BizTalk-Verwaltungsschnittstellen)", "", "", "", "", "", "" }, new object[]