From b9d01339b02b4a81cddf686eca44dba37a29b427 Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Tue, 28 Jul 2026 11:35:10 +0200 Subject: [PATCH] Handle BizTalk artifact collection via ExplorerOM --- Dokumentation.md | 44 +- Readme.md | 24 +- .../Collectors/BizTalkWmiCollector.cs | 453 +++++++++++++++--- .../Collectors/SystemCollector.cs | 10 +- .../Infrastructure/ConsoleFileLogger.cs | 35 +- .../Infrastructure/SafeCollector.cs | 9 +- .../Infrastructure/SelfTestRunner.cs | 129 ++++- src/BizTalkSapEnvironmentInventory/Program.cs | 7 +- .../Reporting/DocxReportWriter.cs | 7 +- 9 files changed, 601 insertions(+), 117 deletions(-) diff --git a/Dokumentation.md b/Dokumentation.md index 072a4f7..387c657 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -41,9 +41,10 @@ Administrative cmd.exe | +-- MSBTS_GroupSetting für Management SQL/DB | +-- BizTalkWmiCollector - | +-- ExplorerOM: vollständige Anwendungsliste + | +-- ExplorerOM: Anwendungen, Assemblies, Schemas, + | | Maps und Pipelines | +-- root\MicrosoftBizTalkServer - | +-- optionale Artefakte, Adapter, Hosts und Handler + | +-- Orchestrierungen, Ports, Adapter, Hosts und Handler | +-- optionaler SAP-Endpunkt-Fallback | +-- BindingExportCollector @@ -104,8 +105,9 @@ SELECT Name,MgmtDbServerName,MgmtDbName FROM MSBTS_GroupSetting Die vollständige Anwendungsliste wird anschließend über `Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer` und integrierte Windows-Authentifizierung aus der BizTalk Management Database gelesen. Es werden -keine ändernden ExplorerOM-Methoden aufgerufen. Die Connection String wird nicht -protokolliert. +keine ändernden ExplorerOM-Methoden aufgerufen. Die bereitgestellten Assemblies +und deren Schemas, Transforms/Maps und Pipelines werden aus denselben +Anwendungsobjekten gelesen. Die Connection String wird nicht protokolliert. Dieser Aufbau vermeidet die nicht verlässlich vorhandene WMI-Klasse `MSBTS_Application`, die je nach Installation mit `Invalid class` abbrechen kann. @@ -114,7 +116,7 @@ ExplorerOM-Assemblyname, Version und Dateipfad werden protokolliert. Wenn `--management-server`/`--management-database` ermittelte Datenbankziel weiterhin für ExplorerOM verwendet werden. -### 3.3 Optionale BizTalk-WMI-Daten +### 3.3 BizTalk-WMI-Laufzeitdaten Primärer Namespace: @@ -122,31 +124,31 @@ Primärer Namespace: root\MicrosoftBizTalkServer ``` -Optionale beziehungsweise versionsabhängige Artefaktklassen: +Verwendete BizTalk-WMI-Klassen: - `MSBTS_Orchestration` - `MSBTS_SendPort` - `MSBTS_SendPortGroup` - `MSBTS_ReceivePort` - `MSBTS_ReceiveLocation` -- `MSBTS_Assembly` -- `MSBTS_Schema` -- `MSBTS_Map` -- `MSBTS_Pipeline` - `MSBTS_AdapterSetting` - `MSBTS_ReceiveHandler` - `MSBTS_SendHandler2` bzw. `MSBTS_SendHandler` - `MSBTS_HostSetting` - `MSBTS_HostInstance` -Eine fehlende optionale Klasse erzeugt einen Hinweis, verhindert aber nicht die -ExplorerOM-Anwendungsliste oder den Word-Bericht. Ist der gesamte BizTalk-WMI- -Namespace für diese Zusatzdaten nicht erreichbar, wird der WMI-Teil übersprungen; -die zuvor geladene Anwendungsliste bleibt erhalten. +`MSBTS_Assembly`, `MSBTS_Schema`, `MSBTS_Map` und `MSBTS_Pipeline` werden bewusst +nicht abgefragt: Sie waren auf ACC nicht registriert und gehören nicht zu den +dokumentierten WMI-Kernklassen. Die entsprechenden Daten stammen aus ExplorerOM. -Zum Schutz vor ungewöhnlich großen Artefaktmengen begrenzt `MaxArtifactDetailsPerType` -die aufgenommenen Detaildatensätze pro WMI-Klasse auf standardmäßig 5000. Die -Anwendungsliste selbst wird nicht begrenzt. +Eine fehlende optionale Laufzeitklasse erzeugt einen Hinweis, verhindert aber +nicht die ExplorerOM-Anwendungsliste oder den Word-Bericht. Ist der gesamte +BizTalk-WMI-Namespace für diese Zusatzdaten nicht erreichbar, wird der WMI-Teil +übersprungen; die zuvor geladene Anwendungsliste bleibt erhalten. + +Zum Schutz vor ungewöhnlich großen Artefaktmengen begrenzt +`MaxArtifactDetailsPerType` die aufgenommenen Detaildatensätze je Artefakttyp auf +standardmäßig 5000. Die Anwendungsliste selbst wird nicht begrenzt. ### 3.4 BTSTask-Gruppenbinding @@ -337,13 +339,16 @@ Das DR-Runbook muss mindestens enthalten: - WMI-Timeout, WQL-, Laufzeit- und Datensatzprotokollierung - vollständige Anwendungsliste über read-only ExplorerOM statt `MSBTS_Application` +- Assemblies, Schemas, Maps und Pipelines über ExplorerOM statt nicht + registrierter WMI-Klassen +- Reflection unterstützt auch explizit implementierte ExplorerOM-Interfaces - dynamische ExplorerOM-Auflösung aus GAC und BizTalk-Installationspfad - WMI-Zusatzdaten dürfen nach erfolgreicher ExplorerOM-Erfassung ausfallen - Prozess-Timeout für `BTSTask` - live gespiegelte, secret-bereinigte BTSTask-Ausgabe -- WMI-Fallback für SAP-Endpunkte +- WMI-Fallback für SAP-Endpunkte ohne doppelte Port-Abfragen - Offline-Bindingmodus -- optionale WMI-Klassen stoppen den Lauf nicht +- optionale Zugriffsfehler werden als Warnung protokolliert und stoppen den Lauf nicht - begrenzte Dateisystemsuche - Fehlerisolation bei Registry, Zertifikaten und Dateimetadaten - XML-Parser ohne DTD/External Entities @@ -420,6 +425,7 @@ Tests prüfen: - SAP-Client, Host und `UseSnc` - Anwendungszuordnung - Diagnose-Logging mit HRESULT und innerer Reflection-Ausnahme +- ExplorerOM-Artefakte einschließlich expliziter Interface-Eigenschaften - DOCX-Paketstruktur - XML-Gültigkeit aller DOCX-Parts - Ausschluss von Testkennwörtern diff --git a/Readme.md b/Readme.md index 5f37797..d3c0a7d 100644 --- a/Readme.md +++ b/Readme.md @@ -22,7 +22,7 @@ BizTalk-Installationspfad geladen. - Anzahl und Zuordnung von Orchestrierungen - Send Ports und Send Port Groups - Receive Ports und Receive Locations -- Assemblies, Schemas, Maps und Pipelines, soweit die jeweilige WMI-Klasse verfügbar ist +- Assemblies, Schemas, Maps und Pipelines aus ExplorerOM - SAP-/IDoc-Indikatoren aus bereitgestellten Schemas, Actions und Artefaktnamen Die Liste ist nicht auf eine bisher bekannte 15-App-Liste begrenzt. Damit werden auch weitere Anwendungen wie beispielsweise `MasterData_SAP_WebGIS`, `MasterDataHeat`, `WoWHeatInvoice` oder `MeterChangeInfo/Order` sichtbar, sofern sie tatsächlich in der jeweiligen BizTalk-Gruppe installiert sind. @@ -139,6 +139,7 @@ Das Tool zeigt jeden Abschnitt und die erzeugten Dateien direkt auf der Konsole [INFO] Suche die lokale Assembly Microsoft.BizTalk.ExplorerOM.dll. [INFO] ExplorerOM-Assembly geladen: Microsoft.BizTalk.ExplorerOM, Version=... [INFO] 34 BizTalk-Anwendung(en) geladen (412 ms). +[INFO] ExplorerOM Schema: 87 Artefakt(e). [INFO] Verbinde lokal mit dem BizTalk-WMI-Namespace für optionale Zusatzdaten. [INFO] WMI-Abfrage abgeschlossen: SendPort; Datensätze=91; Dauer=173 ms. [INFO] Exportiere BizTalk-Gruppenbindings read-only mit BTSTask. @@ -147,11 +148,12 @@ Das Tool zeigt jeden Abschnitt und die erzeugten Dateien direkt auf der Konsole [INFO] Ergebnisübersicht: Anwendungen=34; SAP-Endpunkte=12; Findings=4. ``` -Konsole und Log erhalten dieselben Fortschritts- und Diagnosemeldungen. Fehler -enthalten Ausnahmetyp, HRESULT, WMI-Status, innere Ausnahmen und Stacktrace. -Fehler eines Collectors stoppen die übrigen Abschnitte nicht. Fehlende optionale -WMI-Klassen verwerfen insbesondere nicht die über ExplorerOM geladene -Anwendungsliste. +Konsole und Log erhalten dieselben Fortschritts- und Diagnosemeldungen. +Pflichtfehler erscheinen als `[FEHLER]`, durch einen Fallback abgedeckte oder +optionale Zugriffsfehler als `[WARNUNG]`. Ausnahmedetails enthalten Typ, HRESULT, +WMI-Status, innere Ausnahmen und Stacktrace. Fehler eines Collectors stoppen die +übrigen Abschnitte nicht; Fehler optionaler Zusatzdaten verwerfen insbesondere +nicht die über ExplorerOM geladene Anwendungsliste. ## Ausgabedateien @@ -226,10 +228,12 @@ Die Gitea-Workflowdatei `.gitea/workflows/build.yml` baut, testet und veröffent ### WMI meldet `Invalid class` Die Anwendungsliste verwendet nicht mehr die nicht verlässlich vorhandene Klasse -`MSBTS_Application`, sondern ExplorerOM. Ein `Invalid class` für eine optionale -Artefaktklasse erscheint mit WQL, HRESULT und WMI-Status im Log, wird als Finding -übernommen und der Lauf wird fortgesetzt. Schlägt bereits -`MSBTS_GroupSetting` fehl, kann die Management-Datenbank über die beiden +`MSBTS_Application`, sondern ExplorerOM. Auch Assemblies, Schemas, Maps und +Pipelines werden über ExplorerOM gelesen, weil `MSBTS_Assembly`, `MSBTS_Schema`, +`MSBTS_Map` und `MSBTS_Pipeline` nicht zu den verlässlich registrierten +BizTalk-WMI-Kernklassen gehören. Verbleibende optionale WMI-Fehler erscheinen mit +WQL, HRESULT und WMI-Status als `[WARNUNG]`; der Lauf wird fortgesetzt. Schlägt +bereits `MSBTS_GroupSetting` fehl, kann die Management-Datenbank über die beiden `--management-*`-Optionen vorgegeben werden. ### ExplorerOM-Assembly fehlt diff --git a/src/BizTalkSapEnvironmentInventory/Collectors/BizTalkWmiCollector.cs b/src/BizTalkSapEnvironmentInventory/Collectors/BizTalkWmiCollector.cs index 20b378d..13840b5 100644 --- a/src/BizTalkSapEnvironmentInventory/Collectors/BizTalkWmiCollector.cs +++ b/src/BizTalkSapEnvironmentInventory/Collectors/BizTalkWmiCollector.cs @@ -15,8 +15,8 @@ using BizTalkSapEnvironmentInventory.Models; namespace BizTalkSapEnvironmentInventory.Collectors { /// - /// Liest die vollständige Anwendungsliste über ExplorerOM und ergänzt sie um - /// optionale Artefakt-, Adapter- und Hostdaten aus dem BizTalk-WMI-Provider. + /// Liest Anwendungen sowie bereitgestellte Assembly-Artefakte über ExplorerOM + /// und ergänzt sie um die vom BizTalk-WMI-Provider angebotenen Laufzeitdaten. /// internal sealed class BizTalkWmiCollector { @@ -36,11 +36,31 @@ namespace BizTalkSapEnvironmentInventory.Collectors ConsoleFileLogger logger, int timeoutSeconds, int maxArtifactDetailsPerType) + : this( + document, + logger, + timeoutSeconds, + maxArtifactDetailsPerType, + true) + { + } + + private BizTalkWmiCollector( + InventoryDocument document, + ConsoleFileLogger logger, + int timeoutSeconds, + int maxArtifactDetailsPerType, + bool initializeWmi) { this.document = document; this.logger = logger; timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds)); this.maxArtifactDetailsPerType = Math.Max(100, maxArtifactDetailsPerType); + if (!initializeWmi) + { + return; + } + scope = new ManagementScope(WmiNamespace); scope.Options.Timeout = timeout; scope.Options.EnablePrivileges = false; @@ -110,6 +130,8 @@ namespace BizTalkSapEnvironmentInventory.Collectors logger.Info(applications.Count + " BizTalk-Anwendung(en) geladen (" + timer.ElapsedMilliseconds + " ms)."); + var explorerArtifactCounts = + new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var item in applications) { var name = RequiredText(item, "Name"); @@ -118,6 +140,10 @@ namespace BizTalkSapEnvironmentInventory.Collectors application.Status = FormatStatus(OptionalText(item, "Status")); logger.Detail("ExplorerOM-Anwendung: " + name + "; Status=" + application.Status + "."); + CollectExplorerArtifacts( + item, + application, + explorerArtifactCounts); } if (document.Applications.Count == 0) @@ -125,6 +151,14 @@ namespace BizTalkSapEnvironmentInventory.Collectors throw new InvalidOperationException( "Das BizTalk Explorer Object Model lieferte keine Anwendungen."); } + + foreach (var artifactType in new[] { "Assembly", "Schema", "Map", "Pipeline" }) + { + int count; + explorerArtifactCounts.TryGetValue(artifactType, out count); + logger.Info("ExplorerOM " + artifactType + ": " + + count + " Artefakt(e)."); + } } finally { @@ -145,11 +179,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors new ClassDescriptor("MSBTS_SendPort", "SendPort"), new ClassDescriptor("MSBTS_SendPortGroup", "SendPortGroup"), new ClassDescriptor("MSBTS_ReceivePort", "ReceivePort"), - new ClassDescriptor("MSBTS_ReceiveLocation", "ReceiveLocation"), - new ClassDescriptor("MSBTS_Assembly", "Assembly"), - new ClassDescriptor("MSBTS_Schema", "Schema"), - new ClassDescriptor("MSBTS_Map", "Map"), - new ClassDescriptor("MSBTS_Pipeline", "Pipeline") + new ClassDescriptor("MSBTS_ReceiveLocation", "ReceiveLocation") }; foreach (var descriptor in artifactClasses) @@ -165,9 +195,6 @@ namespace BizTalkSapEnvironmentInventory.Collectors } TryCollectComponents("MSBTS_HostSetting", "Host", document.Hosts, false); TryCollectComponents("MSBTS_HostInstance", "HostInstance", document.Hosts, false); - - TryCollectWmiSapEndpoints("MSBTS_SendPort", "Senden"); - TryCollectWmiSapEndpoints("MSBTS_ReceiveLocation", "Empfangen"); } private void LogRuntimeInformation() @@ -233,7 +260,9 @@ namespace BizTalkSapEnvironmentInventory.Collectors + "Die über ExplorerOM geladene Anwendungsliste bleibt verwendbar.", Owner = "BizTalk-Betrieb" }); - logger.Exception("BizTalk-WMI-Verbindung fehlgeschlagen", exception); + logger.WarningException( + "BizTalk-WMI-Verbindung fehlgeschlagen", + exception); logger.Warning( "Optionale WMI-Zusatzdaten werden übersprungen; " + "die ExplorerOM-Anwendungsliste bleibt erhalten."); @@ -424,6 +453,24 @@ namespace BizTalkSapEnvironmentInventory.Collectors } } + // SendPort und ReceiveLocation werden bereits hier gelesen. + // Der SAP-Fallback nutzt dieselben Zeilen und belastet den + // WMI-Provider daher nicht mit einer zweiten Vollabfrage. + if (string.Equals( + descriptor.ClassName, + "MSBTS_SendPort", + StringComparison.OrdinalIgnoreCase)) + { + TryAddWmiSapEndpoint(row, "Senden"); + } + else if (string.Equals( + descriptor.ClassName, + "MSBTS_ReceiveLocation", + StringComparison.OrdinalIgnoreCase)) + { + TryAddWmiSapEndpoint(row, "Empfangen"); + } + count++; } @@ -481,61 +528,44 @@ namespace BizTalkSapEnvironmentInventory.Collectors } } - private void TryCollectWmiSapEndpoints(string className, string direction) + private void TryAddWmiSapEndpoint(ManagementObject row, string direction) { - List rows = null; - try + if (!LooksLikeSap(row)) { - rows = Query( - className + " SAP-Fallback", - "SELECT * FROM " + className); - foreach (var row in rows) - { - if (!LooksLikeSap(row)) - { - continue; - } + return; + } - var endpointName = First(row, "Name"); - if (document.SapEndpoints.Any(item => - string.Equals(item.Direction, direction, StringComparison.OrdinalIgnoreCase) - && string.Equals(item.Name, endpointName, StringComparison.OrdinalIgnoreCase))) - { - continue; - } + var endpointName = First(row, "Name"); + if (string.IsNullOrWhiteSpace(endpointName) + || document.SapEndpoints.Any(item => + string.Equals(item.Direction, direction, StringComparison.OrdinalIgnoreCase) + && string.Equals(item.Name, endpointName, StringComparison.OrdinalIgnoreCase))) + { + return; + } - var endpoint = new SapEndpointRecord - { - ApplicationName = First(row, "ApplicationName"), - Direction = direction, - Name = endpointName, - ParentPortName = First(row, "ReceivePortName"), - AdapterName = First( - row, - "PTTransportType", - "AdapterName", - "TransportType"), - HostName = First(row, "HostName", "SendHandler", "ReceiveHandler"), - Status = FormatStatus(First(row, "Status", "IsDisabled")), - Address = SensitiveDataSanitizer.SanitizeText(First( - row, - "PTAddress", - "InboundTransportURL", - "Address")), - Source = "WMI-Fallback" - }; - AddSelectedProperties(row, endpoint.Properties); - document.SapEndpoints.Add(endpoint); - } - } - catch (Exception exception) when (IsRecoverableWmiException(exception)) + var endpoint = new SapEndpointRecord { - AddOptionalWmiFinding(className + " SAP-Fallback", exception); - } - finally - { - DisposeRows(rows); - } + ApplicationName = First(row, "ApplicationName"), + Direction = direction, + Name = endpointName, + ParentPortName = First(row, "ReceivePortName"), + AdapterName = First( + row, + "PTTransportType", + "AdapterName", + "TransportType"), + HostName = First(row, "HostName", "SendHandler", "ReceiveHandler"), + Status = FormatStatus(First(row, "Status", "IsDisabled")), + Address = SensitiveDataSanitizer.SanitizeText(First( + row, + "PTAddress", + "InboundTransportURL", + "Address")), + Source = "WMI-Fallback" + }; + AddSelectedProperties(row, endpoint.Properties); + document.SapEndpoints.Add(endpoint); } private List Query(string name, string query) @@ -591,6 +621,247 @@ namespace BizTalkSapEnvironmentInventory.Collectors return result; } + /// + /// Liest Assemblies und deren Schemas, Maps und Pipelines über ExplorerOM. + /// Diese Objekte sind keine verlässlich verfügbaren BizTalk-WMI-Klassen. + /// Fehler in den Zusatzsammlungen dürfen die Anwendungsliste nicht verwerfen. + /// + private void CollectExplorerArtifacts( + object applicationObject, + ApplicationRecord application, + IDictionary counts) + { + foreach (var assembly in EnumerateOptionalProperty( + applicationObject, + "Assemblies", + "Assemblies der Anwendung " + application.Name)) + { + var assemblyName = FirstOptionalText(assembly, "DisplayName", "Name"); + TryAddExplorerArtifact( + application, + counts, + "Assembly", + assemblyName, + string.Empty); + + CollectExplorerAssemblyArtifacts( + assembly, + application, + counts, + "Schemas", + "Schema", + assemblyName); + CollectExplorerAssemblyArtifacts( + assembly, + application, + counts, + "Transforms", + "Map", + assemblyName); + CollectExplorerAssemblyArtifacts( + assembly, + application, + counts, + "Pipelines", + "Pipeline", + assemblyName); + } + } + + private void CollectExplorerAssemblyArtifacts( + object assembly, + ApplicationRecord application, + IDictionary counts, + string collectionProperty, + string artifactType, + string assemblyName) + { + foreach (var item in EnumerateOptionalProperty( + assembly, + collectionProperty, + collectionProperty + " aus Assembly " + assemblyName)) + { + var name = FirstOptionalText( + item, + "FullName", + "AssemblyQualifiedName", + "Name"); + TryAddExplorerArtifact( + application, + counts, + artifactType, + name, + assemblyName); + } + } + + private void TryAddExplorerArtifact( + ApplicationRecord application, + IDictionary counts, + string artifactType, + string name, + string assemblyName) + { + if (string.IsNullOrWhiteSpace(name)) + { + logger.Detail("ExplorerOM-" + artifactType + + " ohne auswertbaren Namen wurde übersprungen."); + return; + } + + int count; + counts.TryGetValue(artifactType, out count); + if (count >= maxArtifactDetailsPerType) + { + AddArtifactLimitFindingOnce(artifactType); + return; + } + + var artifact = new ArtifactRecord + { + Type = artifactType, + Name = name.Trim(), + ApplicationName = application.Name, + Status = "Bereitgestellt" + }; + artifact.Properties.Add(new NameValueRecord( + "Quelle", + "BizTalk Explorer Object Model", + "ExplorerOM")); + if (!string.IsNullOrWhiteSpace(assemblyName) + && !string.Equals(name, assemblyName, StringComparison.OrdinalIgnoreCase)) + { + artifact.Properties.Add(new NameValueRecord( + "Assembly", + assemblyName, + "ExplorerOM")); + } + + application.Artifacts.Add(artifact); + counts[artifactType] = count + 1; + } + + private void AddArtifactLimitFindingOnce(string artifactType) + { + if (document.Findings.Any(item => + string.Equals(item.Area, artifactType, StringComparison.OrdinalIgnoreCase) + && item.Message.IndexOf( + "Artefaktdetails wurden bei", + StringComparison.OrdinalIgnoreCase) >= 0)) + { + return; + } + + document.Findings.Add(new Finding + { + Severity = "Warnung", + Area = artifactType, + Message = "Artefaktdetails wurden bei " + + maxArtifactDetailsPerType + + " Einträgen begrenzt.", + RecommendedAction = + "MaxArtifactDetailsPerType bei Bedarf kontrolliert erhöhen.", + Owner = "BizTalk-Betrieb" + }); + } + + private IList EnumerateOptionalProperty( + object instance, + string propertyName, + string description) + { + try + { + var property = FindProperty(instance, propertyName); + if (property == null) + { + throw new MissingMemberException( + instance.GetType().FullName, + propertyName); + } + + var enumerable = property.GetValue(instance, null) as IEnumerable; + if (enumerable == null) + { + throw new InvalidOperationException( + description + " ist keine aufzählbare Sammlung."); + } + + var result = new List(); + foreach (var item in enumerable) + { + if (item != null) + { + result.Add(item); + } + } + + return result; + } + catch (Exception exception) when (IsOptionalExplorerException(exception)) + { + AddOptionalExplorerFinding(propertyName, description, exception); + return new List(); + } + } + + private void AddOptionalExplorerFinding( + string propertyName, + string description, + Exception exception) + { + var area = "ExplorerOM " + propertyName; + if (!document.Findings.Any(item => + string.Equals(item.Area, area, StringComparison.OrdinalIgnoreCase))) + { + document.Findings.Add(new Finding + { + Severity = "Warnung", + Area = area, + Message = description + " konnten nicht gelesen werden: " + + exception.Message, + RecommendedAction = + "ExplorerOM-Version, Berechtigung und Management-Datenbankzugriff " + + "anhand des Logs prüfen. Die Anwendungsliste bleibt verwendbar.", + Owner = "BizTalk-Betrieb" + }); + logger.WarningException( + "Optionale ExplorerOM-Sammlung " + description + + " konnte nicht gelesen werden", + exception); + } + else + { + logger.Detail("Weitere optionale ExplorerOM-Sammlung " + + description + " konnte nicht gelesen werden: " + + exception.Message); + } + } + + // Wird vom Self-Test mit expliziten Interface-Eigenschaften ausgeführt. + internal void CollectExplorerArtifactsForTest( + object applicationObject, + ApplicationRecord application) + { + CollectExplorerArtifacts( + applicationObject, + application, + new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + + internal static BizTalkWmiCollector CreateForExplorerArtifactTest( + InventoryDocument document, + ConsoleFileLogger logger, + int maxArtifactDetailsPerType) + { + return new BizTalkWmiCollector( + document, + logger, + 30, + maxArtifactDetailsPerType, + false); + } + private static string RequiredText(object instance, string propertyName) { var value = Convert.ToString( @@ -607,9 +878,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors private string OptionalText(object instance, string propertyName) { - var property = instance.GetType().GetProperty( - propertyName, - BindingFlags.Instance | BindingFlags.Public); + var property = FindProperty(instance, propertyName); if (property == null) { return string.Empty; @@ -621,10 +890,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors property.GetValue(instance, null), CultureInfo.InvariantCulture) ?? string.Empty; } - catch (Exception exception) when ( - exception is TargetInvocationException - || exception is COMException - || exception is NotSupportedException) + catch (Exception exception) when (IsOptionalExplorerException(exception)) { logger.Detail("Optionale ExplorerOM-Eigenschaft " + instance.GetType().FullName @@ -636,11 +902,23 @@ namespace BizTalkSapEnvironmentInventory.Collectors } } + private string FirstOptionalText(object instance, params string[] propertyNames) + { + foreach (var propertyName in propertyNames) + { + var value = OptionalText(instance, propertyName); + if (!string.IsNullOrWhiteSpace(value)) + { + return value; + } + } + + return string.Empty; + } + private static object RequiredProperty(object instance, string propertyName) { - var property = instance.GetType().GetProperty( - propertyName, - BindingFlags.Instance | BindingFlags.Public); + var property = FindProperty(instance, propertyName); if (property == null) { throw new MissingMemberException(instance.GetType().FullName, propertyName); @@ -654,9 +932,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors string propertyName, object value) { - var property = instance.GetType().GetProperty( - propertyName, - BindingFlags.Instance | BindingFlags.Public); + var property = FindProperty(instance, propertyName); if (property == null || !property.CanWrite) { throw new MissingMemberException(instance.GetType().FullName, propertyName); @@ -665,6 +941,23 @@ namespace BizTalkSapEnvironmentInventory.Collectors property.SetValue(instance, value, null); } + private static PropertyInfo FindProperty(object instance, string propertyName) + { + var flags = BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic; + var properties = instance.GetType().GetProperties(flags); + return properties.FirstOrDefault(item => + string.Equals( + item.Name, + propertyName, + StringComparison.Ordinal)) + ?? properties.FirstOrDefault(item => + item.Name.EndsWith( + "." + propertyName, + StringComparison.Ordinal)); + } + private ApplicationRecord GetOrCreateApplication(string name) { var application = document.Applications.FirstOrDefault(item => @@ -808,11 +1101,21 @@ namespace BizTalkSapEnvironmentInventory.Collectors RecommendedAction = "Nur relevant, wenn die zugehörige Artefaktspalte vollständig benötigt wird.", Owner = "BizTalk-Betrieb" }); - logger.Exception( + logger.WarningException( "Optionale WMI-Abfrage " + className + " fehlgeschlagen", exception); } + private static bool IsOptionalExplorerException(Exception exception) + { + // Die Sammlungen sind Zusatzdaten. Auch versionsspezifische + // ExplorerOM-/SQL-Ausnahmen sollen daher nur diese Sammlung auslassen. + // Prozesskritische Laufzeitfehler werden weiterhin nicht abgefangen. + return !(exception is OutOfMemoryException) + && !(exception is AccessViolationException) + && !(exception is AppDomainUnloadedException); + } + private static bool IsRecoverableWmiException(Exception exception) { return exception is ManagementException diff --git a/src/BizTalkSapEnvironmentInventory/Collectors/SystemCollector.cs b/src/BizTalkSapEnvironmentInventory/Collectors/SystemCollector.cs index 7148d65..de3224a 100644 --- a/src/BizTalkSapEnvironmentInventory/Collectors/SystemCollector.cs +++ b/src/BizTalkSapEnvironmentInventory/Collectors/SystemCollector.cs @@ -165,7 +165,9 @@ namespace BizTalkSapEnvironmentInventory.Collectors + "die BizTalk-Erfassung wird fortgesetzt.", Owner = "Windows-/BizTalk-Betrieb" }); - logger.Exception("Windows-WMI-Abfrage fehlgeschlagen", exception); + logger.WarningException( + "Windows-WMI-Abfrage fehlgeschlagen", + exception); } } @@ -239,7 +241,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors + "--management-Parameter können die Ermittlung übernehmen.", Owner = "Windows-/BizTalk-Betrieb" }); - logger.Exception( + logger.WarningException( "BizTalk-Registryansicht " + view + " fehlgeschlagen", exception); } @@ -330,7 +332,9 @@ namespace BizTalkSapEnvironmentInventory.Collectors document, "MSBTS_GroupSetting konnte für die Management-DB-Erkennung " + "nicht gelesen werden: " + exception.Message); - logger.Exception("MSBTS_GroupSetting fehlgeschlagen", exception); + logger.WarningException( + "MSBTS_GroupSetting fehlgeschlagen", + exception); } } diff --git a/src/BizTalkSapEnvironmentInventory/Infrastructure/ConsoleFileLogger.cs b/src/BizTalkSapEnvironmentInventory/Infrastructure/ConsoleFileLogger.cs index 5ec4e33..57fdb79 100644 --- a/src/BizTalkSapEnvironmentInventory/Infrastructure/ConsoleFileLogger.cs +++ b/src/BizTalkSapEnvironmentInventory/Infrastructure/ConsoleFileLogger.cs @@ -47,14 +47,45 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure /// WMI-Status und Stacktrace, damit Providerfehler vor Ort analysierbar bleiben. /// public void Exception(string context, Exception exception) + { + WriteException(context, exception, false); + } + + /// + /// Protokolliert eine vollständige Ausnahmekette als Warnung, wenn ein + /// optionaler oder durch einen Fallback abgedeckter Zugriff fehlschlägt. + /// + public void WarningException(string context, Exception exception) + { + WriteException(context, exception, true); + } + + private void WriteException( + string context, + Exception exception, + bool warning) { if (exception == null) { - Error(context + ": Unbekannter Fehler ohne Ausnahmeobjekt."); + if (warning) + { + Warning(context + ": Unbekannter Fehler ohne Ausnahmeobjekt."); + } + else + { + Error(context + ": Unbekannter Fehler ohne Ausnahmeobjekt."); + } return; } - Error(context + ": " + exception.Message); + if (warning) + { + Warning(context + ": " + exception.Message); + } + else + { + Error(context + ": " + exception.Message); + } var current = exception; var depth = 0; while (current != null) diff --git a/src/BizTalkSapEnvironmentInventory/Infrastructure/SafeCollector.cs b/src/BizTalkSapEnvironmentInventory/Infrastructure/SafeCollector.cs index 303427d..8a349dc 100644 --- a/src/BizTalkSapEnvironmentInventory/Infrastructure/SafeCollector.cs +++ b/src/BizTalkSapEnvironmentInventory/Infrastructure/SafeCollector.cs @@ -63,7 +63,14 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure + "und lokale BizTalk-Konfiguration prüfen.", Owner = "BizTalk-Betrieb" }); - logger.Exception(name, exception); + if (required) + { + logger.Exception(name, exception); + } + else + { + logger.WarningException(name, exception); + } } } } diff --git a/src/BizTalkSapEnvironmentInventory/Infrastructure/SelfTestRunner.cs b/src/BizTalkSapEnvironmentInventory/Infrastructure/SelfTestRunner.cs index e3848a8..e504ebc 100644 --- a/src/BizTalkSapEnvironmentInventory/Infrastructure/SelfTestRunner.cs +++ b/src/BizTalkSapEnvironmentInventory/Infrastructure/SelfTestRunner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.IO; using System.IO.Compression; using System.Linq; @@ -24,6 +25,7 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure { TestSanitizer(); TestBindingParser(); + TestExplorerArtifactReader(); TestLoggerDiagnostics(); TestDocxPackage(); } @@ -238,6 +240,53 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure } } + private static void TestExplorerArtifactReader() + { + var directory = CreateTemporaryDirectory(); + var logPath = Path.Combine(directory, "explorer-artifacts.log"); + try + { + var document = new InventoryDocument(); + var application = new ApplicationRecord + { + Name = "ExplorerOM-Test", + Status = "Gestartet" + }; + using (var logger = new ConsoleFileLogger(logPath)) + { + var collector = BizTalkWmiCollector.CreateForExplorerArtifactTest( + document, + logger, + 5000); + collector.CollectExplorerArtifactsForTest( + new FakeApplication(new FakeAssembly( + "Test.Assembly, Version=1.0.0.0", + new FakeArtifact("Test.Schema"), + new FakeArtifact("Test.Map"), + new FakeArtifact("Test.Pipeline"))), + application); + } + + Assert(application.Count("Assembly") == 1, + "ExplorerOM-Assembly wurde nicht gelesen."); + Assert(application.Count("Schema") == 1, + "ExplorerOM-Schema wurde nicht gelesen."); + Assert(application.Count("Map") == 1, + "ExplorerOM-Map wurde nicht gelesen."); + Assert(application.Count("Pipeline") == 1, + "ExplorerOM-Pipeline wurde nicht gelesen."); + Assert( + application.Artifacts.All(item => + item.Properties.Any(property => + property.Value == "BizTalk Explorer Object Model")), + "ExplorerOM-Quelle fehlt an einem Artefakt."); + } + finally + { + DeleteDirectory(directory); + } + } + private static void TestLoggerDiagnostics() { var directory = CreateTemporaryDirectory(); @@ -254,7 +303,7 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure // Die absichtlich erzeugte Testausnahme soll den Self-Test // auf der Konsole nicht wie einen echten Laufzeitfehler aussehen lassen. Console.SetOut(TextWriter.Null); - logger.Exception("Self-Test-Diagnose", exception); + logger.WarningException("Self-Test-Diagnose", exception); } finally { @@ -272,6 +321,10 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure Assert( log.Contains("HRESULT=0x"), "Logger dokumentierte den HRESULT nicht."); + Assert( + log.Contains("[WARNUNG] Self-Test-Diagnose") + && !log.Contains("[FEHLER] Self-Test-Diagnose"), + "Optionale Ausnahme wurde nicht als Warnung protokolliert."); } finally { @@ -381,5 +434,79 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure throw new InvalidOperationException(message); } } + + private interface IFakeApplication + { + IEnumerable Assemblies { get; } + } + + private interface IFakeAssembly + { + IEnumerable Schemas { get; } + IEnumerable Transforms { get; } + IEnumerable Pipelines { get; } + } + + private sealed class FakeApplication : IFakeApplication + { + private readonly IEnumerable assemblies; + + public FakeApplication(params object[] assemblies) + { + this.assemblies = assemblies; + } + + // ExplorerOM stellt einige Sammlungen explizit über Interfaces bereit. + IEnumerable IFakeApplication.Assemblies + { + get { return assemblies; } + } + } + + private sealed class FakeAssembly : IFakeAssembly + { + private readonly IEnumerable schemas; + private readonly IEnumerable transforms; + private readonly IEnumerable pipelines; + + public FakeAssembly( + string displayName, + object schema, + object transform, + object pipeline) + { + DisplayName = displayName; + schemas = new[] { schema }; + transforms = new[] { transform }; + pipelines = new[] { pipeline }; + } + + public string DisplayName { get; private set; } + + IEnumerable IFakeAssembly.Schemas + { + get { return schemas; } + } + + IEnumerable IFakeAssembly.Transforms + { + get { return transforms; } + } + + IEnumerable IFakeAssembly.Pipelines + { + get { return pipelines; } + } + } + + private sealed class FakeArtifact + { + public FakeArtifact(string fullName) + { + FullName = fullName; + } + + public string FullName { get; private set; } + } } } diff --git a/src/BizTalkSapEnvironmentInventory/Program.cs b/src/BizTalkSapEnvironmentInventory/Program.cs index 107c197..03a1e64 100644 --- a/src/BizTalkSapEnvironmentInventory/Program.cs +++ b/src/BizTalkSapEnvironmentInventory/Program.cs @@ -83,8 +83,9 @@ namespace BizTalkSapEnvironmentInventory logger.Info("Server: " + Environment.MachineName); logger.Info("Ausgabeordner: " + options.OutputDirectory); logger.Info( - "Modus: read-only; ExplorerOM für Anwendungen, WMI für " - + "Gruppenermittlung und optionale Artefakte."); + "Modus: read-only; ExplorerOM für Anwendungen und " + + "bereitgestellte Assembly-Artefakte; WMI für " + + "Gruppenermittlung und Laufzeitobjekte."); logger.Detail("Logdatei: " + logPath); logger.Detail("DOCX-Zieldatei: " + reportPath); @@ -105,7 +106,7 @@ namespace BizTalkSapEnvironmentInventory logger, wmiTimeoutSeconds).Collect(document)); safeCollector.Execute( - "BizTalk-Anwendungen und optionale WMI-Artefakte", + "BizTalk-Anwendungen und -Artefakte", true, () => new BizTalkWmiCollector( document, diff --git a/src/BizTalkSapEnvironmentInventory/Reporting/DocxReportWriter.cs b/src/BizTalkSapEnvironmentInventory/Reporting/DocxReportWriter.cs index d550996..f09ceca 100644 --- a/src/BizTalkSapEnvironmentInventory/Reporting/DocxReportWriter.cs +++ b/src/BizTalkSapEnvironmentInventory/Reporting/DocxReportWriter.cs @@ -168,9 +168,10 @@ namespace BizTalkSapEnvironmentInventory.Reporting WriteParagraph( writer, "Die vollständige Anwendungsliste stammt aus dem read-only BizTalk Explorer " - + "Object Model. Artefaktzahlen werden aus lokal verfügbaren, optionalen " - + "BizTalk-WMI-Klassen gebildet; fehlende Klassen werden im Log und als " - + "Finding ausgewiesen, ohne die Anwendungsliste zu verwerfen.", + + "Object Model. Assemblies, Schemas, Maps und Pipelines werden ebenfalls " + + "über ExplorerOM gelesen. Laufzeitobjekte wie Ports und Orchestrierungen " + + "stammen aus den lokal verfügbaren BizTalk-WMI-Klassen. Fehler optionaler " + + "Zusatzdaten verwerfen die Anwendungsliste nicht.", "Normal", false, null);