Fix BizTalk catalog collection and diagnostics
Build und Test / build (push) Has been cancelled

This commit is contained in:
2026-07-28 09:29:57 +02:00
parent 53fcfb84a4
commit 6db6e60121
7 changed files with 683 additions and 148 deletions
+101 -30
View File
@@ -21,7 +21,7 @@ ACC und PROD besitzen jeweils einen lokalen BizTalk Server 2020. Ein Prozess auf
```text ```text
ACC BizTalk Server PROD BizTalk Server ACC BizTalk Server PROD BizTalk Server
| | | |
| lokales WMI | lokales WMI | WMI + ExplorerOM | WMI + ExplorerOM
v v v v
ACC JSON + lokale XLSX PROD JSON + lokale XLSX ACC JSON + lokale XLSX PROD JSON + lokale XLSX
\ / \ /
@@ -42,10 +42,9 @@ CommandLineOptions
+-- validiert lokalen Modus oder Merge-Modus +-- validiert lokalen Modus oder Merge-Modus
BizTalkWmiCollector BizTalkWmiCollector
+-- MSBTS_Application +-- lokales MSBTS_GroupSetting per WMI
+-- MSBTS_ReceivePort +-- lädt Microsoft.BizTalk.ExplorerOM aus GAC/Installationspfad
+-- MSBTS_SendPort +-- Anwendungen und Ports per BtsCatalogExplorer
+-- MSBTS_ReceiveLocation
SnapshotStore SnapshotStore
+-- normalisiert je Anwendung Adapterzählungen +-- normalisiert je Anwendung Adapterzählungen
@@ -58,37 +57,83 @@ XlsxReportWriter
+-- Titel, Filter, Freeze Pane und Summenzeile +-- 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` ### 4.3 Kataloglesung per Explorer Object Model
- Fallback `PrimaryTransportType`
- Fallback `AdapterName`
- Fallback `TransportType`
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` Ladestrategie:
- Fallback `SecondaryTransportType`
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 ## 5. Snapshot-Format
@@ -170,7 +215,8 @@ Der Filter umfasst nur Kopf- und Datenzeilen, nicht die Summenzeile. Die ersten
## 8. Fehlerverhalten ## 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` - Exitcode `1`
- Logeintrag - 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`. 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 ## 9. Sicherheit
- ausschließlich lesende lokale WMI-Abfragen - ausschließlich lesende lokale WMI-Abfragen
- keine Änderungen an BizTalk-Konfiguration oder Runtime - ausschließlich lesende Katalogabfragen über das lokale BizTalk Explorer Object Model
- keine SQL-Abfragen - 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 Endpunktadressen
- keine Binding-Exporte - keine Binding-Exporte
- keine Credentials, Secrets oder Tokens - keine Credentials, Secrets oder Tokens
@@ -203,6 +271,7 @@ Baseline:
- C# 7.3 - C# 7.3
- keine NuGet-Pakete - keine NuGet-Pakete
- kein .NET SDK erforderlich - kein .NET SDK erforderlich
- keine compile-time-Referenz auf `Microsoft.BizTalk.ExplorerOM.dll`
```cmd ```cmd
scripts\build-release.cmd scripts\build-release.cmd
@@ -227,10 +296,12 @@ Je Umgebung:
1. `--self-test` erfolgreich ausführen. 1. `--self-test` erfolgreich ausführen.
2. Lokalen Lauf in administrativer `cmd.exe` starten. 2. Lokalen Lauf in administrativer `cmd.exe` starten.
3. Exitcode `0` und Log prüfen. 3. Im Log die erfolgreiche WMI-Abfrage `MSBTS_GroupSetting` prüfen.
4. Anwendungsgesamtzahl mit der BizTalk Administration Console vergleichen. 4. Im Log Assemblyname, Version und Pfad von ExplorerOM prüfen.
5. Send Ports, sekundäre Transporte und Receive Locations stichprobenartig zählen. 5. Exitcode `0` und `IsComplete: true` prüfen.
6. Nur die vollständige JSON-Datei für den Merge verwenden. 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: Nach dem Merge:
+66 -6
View File
@@ -26,12 +26,18 @@ Jeder lokale Lauf erzeugt zusätzlich eine einblättrige Excel-Sicht für die je
## Erfasste Daten ## 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 - WMI-Klasse `MSBTS_GroupSetting` im Namespace
- `MSBTS_SendPort` für primäre und gegebenenfalls sekundäre Send-Endpunkte `root\MicrosoftBizTalkServer`, um Server und Name der BizTalk-Management-Datenbank
- `MSBTS_ReceivePort` nur zur Zuordnung einer Receive Location zur Anwendung der lokalen BizTalk-Gruppe zu ermitteln;
- `MSBTS_ReceiveLocation` für Receive-Endpunkte - 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: Ein Endpunkt ist:
@@ -48,8 +54,13 @@ Nicht erfasst werden insbesondere Adressen, Credentials, Beschreibungen, Status,
- BizTalk Server 2020 auf Windows Server 2019 - BizTalk Server 2020 auf Windows Server 2019
- lokale Ausführung auf dem jeweiligen BizTalk-Server - lokale Ausführung auf dem jeweiligen BizTalk-Server
- .NET Framework 4.7.2 oder höher - .NET Framework 4.7.2 oder höher
- lokal installierte BizTalk-Verwaltungskomponenten einschließlich
`Microsoft.BizTalk.ExplorerOM.dll`
- Leserechte auf `root\MicrosoftBizTalkServer` - 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. 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. 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 ## ACC und PROD zusammenführen
Beide JSON-Dateien in einen gemeinsamen, geschützten Ordner kopieren und ausfü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 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). Technische Details stehen in [Dokumentation.md](Dokumentation.md).
@@ -1,24 +1,33 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Management; using System.Management;
using System.Reflection;
using System.Security.Principal;
using BizTalkApplicationCatalog.Infrastructure; using BizTalkApplicationCatalog.Infrastructure;
using BizTalkApplicationCatalog.Models; using BizTalkApplicationCatalog.Models;
namespace BizTalkApplicationCatalog.Collectors namespace BizTalkApplicationCatalog.Collectors
{ {
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
internal sealed class BizTalkWmiCollector 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 InventoryDocument document;
private readonly ConsoleFileLogger logger; private readonly ConsoleFileLogger logger;
private readonly ManagementScope scope; private readonly ManagementScope scope;
private readonly TimeSpan timeout; private readonly TimeSpan timeout;
private readonly Dictionary<string, string> receivePortApplications =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public BizTalkWmiCollector( public BizTalkWmiCollector(
InventoryDocument document, InventoryDocument document,
@@ -28,18 +37,18 @@ namespace BizTalkApplicationCatalog.Collectors
this.document = document; this.document = document;
this.logger = logger; this.logger = logger;
timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds)); timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds));
scope = new ManagementScope( scope = new ManagementScope(WmiNamespace);
@"\\" + Environment.MachineName + @"\root\MicrosoftBizTalkServer");
scope.Options.Timeout = timeout; scope.Options.Timeout = timeout;
scope.Options.EnablePrivileges = false;
scope.Options.Impersonation = ImpersonationLevel.Impersonate;
} }
public void Collect() public void Collect()
{ {
scope.Connect(); LogRuntimeInformation();
CollectApplications(); ConnectWmi();
CollectReceivePortMappings(); var group = ReadBizTalkGroup();
CollectSendPortEndpoints(); CollectWithExplorerObjectModel(group);
CollectReceiveLocationEndpoints();
document.Applications.Sort((left, right) => document.Applications.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase)); 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"); var principal = new WindowsPrincipal(identity);
if (!string.IsNullOrWhiteSpace(name)) logger.Info("Sicherheitskontext: Benutzer=" + identity.Name
{ + "; administrativ="
document.Applications.Add(new ApplicationRecord { Name = name.Trim() }); + (principal.IsInRole(WindowsBuiltInRole.Administrator)
? "Ja"
: "Nein")
+ ".");
} }
} }
catch (Exception exception)
{
// Ein Diagnosefehler darf die eigentliche Inventarisierung nicht verhindern.
logger.Warning("Sicherheitskontext konnte nicht ermittelt werden: "
+ exception.Message);
}
} }
if (document.Applications.Count == 0) 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_GroupSetting lieferte "
+ rows.Count
+ " Datensätze; exakt ein Datensatz wurde erwartet.");
}
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 CollectWithExplorerObjectModel(BizTalkGroupLocation group)
{
var explorerType = ResolveExplorerType();
object explorer = null;
try
{
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)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"MSBTS_Application lieferte keine Anwendungen."); "Das BizTalk Explorer Object Model lieferte keine Anwendungen.");
} }
logger.Info(document.Applications.Count CollectApplicationsAndEndpoints(applications);
+ " BizTalk-Anwendung(en) gefunden."); }
finally
{
var disposable = explorer as IDisposable;
if (disposable != null)
{
disposable.Dispose();
logger.Detail("BizTalk Explorer Object Model wurde geschlossen.");
}
}
} }
private void CollectReceivePortMappings() private void CollectApplicationsAndEndpoints(IList<object> applications)
{ {
var count = 0; var sendPortCount = 0;
foreach (var row in Query("SELECT * FROM MSBTS_ReceivePort")) var secondaryTransportCount = 0;
{ var receivePortCount = 0;
using (row) var receiveLocationCount = 0;
{
var name = First(row, "Name", "ReceivePortName");
var application = First(row, "ApplicationName", "Application");
if (string.IsNullOrWhiteSpace(name)) continue;
receivePortApplications[name] = application;
count++;
}
}
logger.Info(count + " Receive-Port-Zuordnung(en) gelesen.");
}
private void CollectSendPortEndpoints() foreach (var application in applications)
{ {
var count = 0; var applicationName = RequiredText(application, "Name");
foreach (var row in Query("SELECT * FROM MSBTS_SendPort")) document.Applications.Add(new ApplicationRecord
{ {
using (row) Name = applicationName
{ });
var application = First(row, "ApplicationName", "Application"); logger.Detail("Lese Anwendung: " + applicationName + ".");
AddEndpoint(application, First(
row,
"PTTransportType",
"PrimaryTransportType",
"AdapterName",
"TransportType"));
count++;
var secondaryAdapter = First( var sendPorts = EnumerateRequiredProperty(
row, application,
"STTransportType", "SendPorts",
"SecondaryTransportType"); "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)
{
var secondaryAdapter = AdapterName(secondaryTransport);
// ExplorerOM kann ein leeres TransportInfo-Objekt liefern,
// obwohl kein Backup-Transport konfiguriert ist.
if (!string.IsNullOrWhiteSpace(secondaryAdapter)) if (!string.IsNullOrWhiteSpace(secondaryAdapter))
{ {
AddEndpoint(application, secondaryAdapter); AddEndpoint(applicationName, secondaryAdapter);
count++; secondaryTransportCount++;
logger.Detail("Send Port: Anwendung=" + applicationName
+ "; Name=" + sendPortName
+ "; Sekundäradapter=" + secondaryAdapter + ".");
} }
} }
} }
logger.Info(count + " Send-Endpunkt(e) gefunden.");
}
private void CollectReceiveLocationEndpoints() var receivePorts = EnumerateRequiredProperty(
{
var count = 0;
foreach (var row in Query("SELECT * FROM MSBTS_ReceiveLocation"))
{
using (row)
{
var application = First(row, "ApplicationName", "Application");
if (string.IsNullOrWhiteSpace(application))
{
var receivePort = First(row, "ReceivePortName");
receivePortApplications.TryGetValue(receivePort, out application);
}
AddEndpoint(
application, application,
First(row, "AdapterName", "TransportType", "PTTransportType")); "ReceivePorts",
count++; "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(count + " Receive-Endpunkt(e) gefunden.");
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);
}
}
}
return null;
}
private void LogExplorerAssembly(Assembly assembly)
{
logger.Info("ExplorerOM-Assembly geladen: " + assembly.FullName + ".");
try
{
logger.Detail("ExplorerOM-Assemblypfad: " + assembly.Location);
}
catch (NotSupportedException)
{
logger.Detail("ExplorerOM-Assemblypfad ist für diese Assembly nicht verfügbar.");
}
}
private List<ManagementObject> 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<ManagementObject>().ToList();
timer.Stop();
logger.Info("WMI-Abfrage abgeschlossen: " + name
+ "; Datensätze=" + rows.Count
+ "; Dauer=" + timer.ElapsedMilliseconds + " ms.");
return rows;
}
} }
private void AddEndpoint(string applicationName, string adapterType) 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 document.Endpoints.Add(new EndpointRecord
{ {
ApplicationName = applicationName.Trim(), ApplicationName = applicationName.Trim(),
@@ -169,40 +415,135 @@ namespace BizTalkApplicationCatalog.Collectors
}); });
} }
private List<ManagementObject> Query(string query) private static string AdapterName(object transportTypeOrInfo)
{ {
var options = new EnumerationOptions if (transportTypeOrInfo == null) return string.Empty;
{
ReturnImmediately = false, // Send-Transporte liefern zuerst TransportInfo und darin TransportType.
Rewindable = false, // Receive Locations liefern direkt den ProtocolType.
Timeout = timeout var protocolType = OptionalProperty(
}; transportTypeOrInfo,
using (var searcher = new ManagementObjectSearcher( "TransportType") ?? transportTypeOrInfo;
scope, return Convert.ToString(
new ObjectQuery(query), OptionalProperty(protocolType, "Name"),
options)) CultureInfo.InvariantCulture) ?? string.Empty;
{
return searcher.Get().Cast<ManagementObject>().ToList();
}
} }
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;
{
try
{
var value = row[name];
if (value == null) continue;
var text = Convert.ToString(value, CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(text)) return text;
} }
catch (ManagementException)
private static IList<object> EnumerateRequiredProperty(
object instance,
string propertyName,
string description)
{ {
// WMI-Properties unterscheiden sich zwischen Providerständen. var value = RequiredProperty(instance, propertyName);
var enumerable = value as IEnumerable;
if (enumerable == null)
{
throw new InvalidOperationException(
description + " ist keine aufzählbare Sammlung.");
} }
var result = new List<object>();
foreach (var item in enumerable)
{
if (item != null) result.Add(item);
} }
return string.Empty; 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<ManagementObject> 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; }
} }
} }
} }
@@ -1,6 +1,8 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Management;
using System.Reflection;
using System.Text; using System.Text;
namespace BizTalkApplicationCatalog.Infrastructure namespace BizTalkApplicationCatalog.Infrastructure
@@ -19,8 +21,57 @@ namespace BizTalkApplicationCatalog.Infrastructure
} }
public void Info(string message) { Write("INFO", message); } 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 Warning(string message) { Write("WARNUNG", message); }
public void Error(string message) { Write("FEHLER", message); } public void Error(string message) { Write("FEHLER", message); }
/// <summary>
/// Schreibt die vollständige Ausnahmekette einschließlich HRESULT,
/// WMI-Status und Stacktrace auf Konsole und in die Logdatei.
/// </summary>
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(); } public void Dispose() { writer.Dispose(); }
private void Write(string level, string message) private void Write(string level, string message)
@@ -52,9 +52,11 @@ namespace BizTalkApplicationCatalog.Infrastructure
Severity = required ? "Fehler" : "Warnung", Severity = required ? "Fehler" : "Warnung",
Area = name, Area = name,
Message = "Datenerfassung fehlgeschlagen: " + exception.Message, 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);
} }
} }
} }
+11 -2
View File
@@ -73,7 +73,16 @@ namespace BizTalkApplicationCatalog
logger.Info("BEW BizTalk Application Catalog Phase 1 startet."); logger.Info("BEW BizTalk Application Catalog Phase 1 startet.");
logger.Info("Umgebung: " + options.EnvironmentName); logger.Info("Umgebung: " + options.EnvironmentName);
logger.Info("Erfasst werden nur Anwendungen, Adaptertypen und Endpunkte."); 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( new SafeCollector(document, logger).Execute(
"Phase-1-Anwendungen und Endpunkte", "Phase-1-Anwendungen und Endpunkte",
@@ -81,7 +90,7 @@ namespace BizTalkApplicationCatalog
() => new BizTalkWmiCollector( () => new BizTalkWmiCollector(
document, document,
logger, logger,
ReadInt("WmiTimeoutSeconds", 30)).Collect()); wmiTimeoutSeconds).Collect());
document.CompletedUtc = DateTime.UtcNow; document.CompletedUtc = DateTime.UtcNow;
var snapshot = SnapshotStore.FromInventory( var snapshot = SnapshotStore.FromInventory(
@@ -76,7 +76,8 @@ namespace BizTalkApplicationCatalog.Reporting
{ {
snapshots.Count > 1 snapshots.Count > 1
? "Phase 1 BizTalk Anwendungen & Adapter (Quelle: ACC/PROD JSON-Snapshots)" ? "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[] new object[]