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
@@ -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
{
/// <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>
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<string, string> receivePortApplications =
new Dictionary<string, string>(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<object> 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<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;
}
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<ManagementObject> 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<ManagementObject>().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<object> 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<object>();
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<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.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); }
/// <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(); }
private void Write(string level, string message)
@@ -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);
}
}
}
+11 -2
View File
@@ -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(
@@ -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[]