Handle BizTalk artifact collection via ExplorerOM
Build und Test / build (push) Has been cancelled

This commit is contained in:
2026-07-28 11:35:10 +02:00
parent d1e10920c9
commit b9d01339b0
9 changed files with 601 additions and 117 deletions
+25 -19
View File
@@ -41,9 +41,10 @@ Administrative cmd.exe
| +-- MSBTS_GroupSetting für Management SQL/DB | +-- MSBTS_GroupSetting für Management SQL/DB
| |
+-- BizTalkWmiCollector +-- BizTalkWmiCollector
| +-- ExplorerOM: vollständige Anwendungsliste | +-- ExplorerOM: Anwendungen, Assemblies, Schemas,
| | Maps und Pipelines
| +-- root\MicrosoftBizTalkServer | +-- root\MicrosoftBizTalkServer
| +-- optionale Artefakte, Adapter, Hosts und Handler | +-- Orchestrierungen, Ports, Adapter, Hosts und Handler
| +-- optionaler SAP-Endpunkt-Fallback | +-- optionaler SAP-Endpunkt-Fallback
| |
+-- BindingExportCollector +-- BindingExportCollector
@@ -104,8 +105,9 @@ SELECT Name,MgmtDbServerName,MgmtDbName FROM MSBTS_GroupSetting
Die vollständige Anwendungsliste wird anschließend über Die vollständige Anwendungsliste wird anschließend über
`Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer` und integrierte `Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer` und integrierte
Windows-Authentifizierung aus der BizTalk Management Database gelesen. Es werden Windows-Authentifizierung aus der BizTalk Management Database gelesen. Es werden
keine ändernden ExplorerOM-Methoden aufgerufen. Die Connection String wird nicht keine ändernden ExplorerOM-Methoden aufgerufen. Die bereitgestellten Assemblies
protokolliert. 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 Dieser Aufbau vermeidet die nicht verlässlich vorhandene WMI-Klasse
`MSBTS_Application`, die je nach Installation mit `Invalid class` abbrechen kann. `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 `--management-server`/`--management-database` ermittelte Datenbankziel weiterhin
für ExplorerOM verwendet werden. für ExplorerOM verwendet werden.
### 3.3 Optionale BizTalk-WMI-Daten ### 3.3 BizTalk-WMI-Laufzeitdaten
Primärer Namespace: Primärer Namespace:
@@ -122,31 +124,31 @@ Primärer Namespace:
root\MicrosoftBizTalkServer root\MicrosoftBizTalkServer
``` ```
Optionale beziehungsweise versionsabhängige Artefaktklassen: Verwendete BizTalk-WMI-Klassen:
- `MSBTS_Orchestration` - `MSBTS_Orchestration`
- `MSBTS_SendPort` - `MSBTS_SendPort`
- `MSBTS_SendPortGroup` - `MSBTS_SendPortGroup`
- `MSBTS_ReceivePort` - `MSBTS_ReceivePort`
- `MSBTS_ReceiveLocation` - `MSBTS_ReceiveLocation`
- `MSBTS_Assembly`
- `MSBTS_Schema`
- `MSBTS_Map`
- `MSBTS_Pipeline`
- `MSBTS_AdapterSetting` - `MSBTS_AdapterSetting`
- `MSBTS_ReceiveHandler` - `MSBTS_ReceiveHandler`
- `MSBTS_SendHandler2` bzw. `MSBTS_SendHandler` - `MSBTS_SendHandler2` bzw. `MSBTS_SendHandler`
- `MSBTS_HostSetting` - `MSBTS_HostSetting`
- `MSBTS_HostInstance` - `MSBTS_HostInstance`
Eine fehlende optionale Klasse erzeugt einen Hinweis, verhindert aber nicht die `MSBTS_Assembly`, `MSBTS_Schema`, `MSBTS_Map` und `MSBTS_Pipeline` werden bewusst
ExplorerOM-Anwendungsliste oder den Word-Bericht. Ist der gesamte BizTalk-WMI- nicht abgefragt: Sie waren auf ACC nicht registriert und gehören nicht zu den
Namespace für diese Zusatzdaten nicht erreichbar, wird der WMI-Teil übersprungen; dokumentierten WMI-Kernklassen. Die entsprechenden Daten stammen aus ExplorerOM.
die zuvor geladene Anwendungsliste bleibt erhalten.
Zum Schutz vor ungewöhnlich großen Artefaktmengen begrenzt `MaxArtifactDetailsPerType` Eine fehlende optionale Laufzeitklasse erzeugt einen Hinweis, verhindert aber
die aufgenommenen Detaildatensätze pro WMI-Klasse auf standardmäßig 5000. Die nicht die ExplorerOM-Anwendungsliste oder den Word-Bericht. Ist der gesamte
Anwendungsliste selbst wird nicht begrenzt. 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 ### 3.4 BTSTask-Gruppenbinding
@@ -337,13 +339,16 @@ Das DR-Runbook muss mindestens enthalten:
- WMI-Timeout, WQL-, Laufzeit- und Datensatzprotokollierung - WMI-Timeout, WQL-, Laufzeit- und Datensatzprotokollierung
- vollständige Anwendungsliste über read-only ExplorerOM statt - vollständige Anwendungsliste über read-only ExplorerOM statt
`MSBTS_Application` `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 - dynamische ExplorerOM-Auflösung aus GAC und BizTalk-Installationspfad
- WMI-Zusatzdaten dürfen nach erfolgreicher ExplorerOM-Erfassung ausfallen - WMI-Zusatzdaten dürfen nach erfolgreicher ExplorerOM-Erfassung ausfallen
- Prozess-Timeout für `BTSTask` - Prozess-Timeout für `BTSTask`
- live gespiegelte, secret-bereinigte BTSTask-Ausgabe - live gespiegelte, secret-bereinigte BTSTask-Ausgabe
- WMI-Fallback für SAP-Endpunkte - WMI-Fallback für SAP-Endpunkte ohne doppelte Port-Abfragen
- Offline-Bindingmodus - Offline-Bindingmodus
- optionale WMI-Klassen stoppen den Lauf nicht - optionale Zugriffsfehler werden als Warnung protokolliert und stoppen den Lauf nicht
- begrenzte Dateisystemsuche - begrenzte Dateisystemsuche
- Fehlerisolation bei Registry, Zertifikaten und Dateimetadaten - Fehlerisolation bei Registry, Zertifikaten und Dateimetadaten
- XML-Parser ohne DTD/External Entities - XML-Parser ohne DTD/External Entities
@@ -420,6 +425,7 @@ Tests prüfen:
- SAP-Client, Host und `UseSnc` - SAP-Client, Host und `UseSnc`
- Anwendungszuordnung - Anwendungszuordnung
- Diagnose-Logging mit HRESULT und innerer Reflection-Ausnahme - Diagnose-Logging mit HRESULT und innerer Reflection-Ausnahme
- ExplorerOM-Artefakte einschließlich expliziter Interface-Eigenschaften
- DOCX-Paketstruktur - DOCX-Paketstruktur
- XML-Gültigkeit aller DOCX-Parts - XML-Gültigkeit aller DOCX-Parts
- Ausschluss von Testkennwörtern - Ausschluss von Testkennwörtern
+14 -10
View File
@@ -22,7 +22,7 @@ BizTalk-Installationspfad geladen.
- Anzahl und Zuordnung von Orchestrierungen - Anzahl und Zuordnung von Orchestrierungen
- Send Ports und Send Port Groups - Send Ports und Send Port Groups
- Receive Ports und Receive Locations - 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 - 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. 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] Suche die lokale Assembly Microsoft.BizTalk.ExplorerOM.dll.
[INFO] ExplorerOM-Assembly geladen: Microsoft.BizTalk.ExplorerOM, Version=... [INFO] ExplorerOM-Assembly geladen: Microsoft.BizTalk.ExplorerOM, Version=...
[INFO] 34 BizTalk-Anwendung(en) geladen (412 ms). [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] Verbinde lokal mit dem BizTalk-WMI-Namespace für optionale Zusatzdaten.
[INFO] WMI-Abfrage abgeschlossen: SendPort; Datensätze=91; Dauer=173 ms. [INFO] WMI-Abfrage abgeschlossen: SendPort; Datensätze=91; Dauer=173 ms.
[INFO] Exportiere BizTalk-Gruppenbindings read-only mit BTSTask. [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. [INFO] Ergebnisübersicht: Anwendungen=34; SAP-Endpunkte=12; Findings=4.
``` ```
Konsole und Log erhalten dieselben Fortschritts- und Diagnosemeldungen. Fehler Konsole und Log erhalten dieselben Fortschritts- und Diagnosemeldungen.
enthalten Ausnahmetyp, HRESULT, WMI-Status, innere Ausnahmen und Stacktrace. Pflichtfehler erscheinen als `[FEHLER]`, durch einen Fallback abgedeckte oder
Fehler eines Collectors stoppen die übrigen Abschnitte nicht. Fehlende optionale optionale Zugriffsfehler als `[WARNUNG]`. Ausnahmedetails enthalten Typ, HRESULT,
WMI-Klassen verwerfen insbesondere nicht die über ExplorerOM geladene WMI-Status, innere Ausnahmen und Stacktrace. Fehler eines Collectors stoppen die
Anwendungsliste. übrigen Abschnitte nicht; Fehler optionaler Zusatzdaten verwerfen insbesondere
nicht die über ExplorerOM geladene Anwendungsliste.
## Ausgabedateien ## Ausgabedateien
@@ -226,10 +228,12 @@ Die Gitea-Workflowdatei `.gitea/workflows/build.yml` baut, testet und veröffent
### WMI meldet `Invalid class` ### WMI meldet `Invalid class`
Die Anwendungsliste verwendet nicht mehr die nicht verlässlich vorhandene Klasse Die Anwendungsliste verwendet nicht mehr die nicht verlässlich vorhandene Klasse
`MSBTS_Application`, sondern ExplorerOM. Ein `Invalid class` für eine optionale `MSBTS_Application`, sondern ExplorerOM. Auch Assemblies, Schemas, Maps und
Artefaktklasse erscheint mit WQL, HRESULT und WMI-Status im Log, wird als Finding Pipelines werden über ExplorerOM gelesen, weil `MSBTS_Assembly`, `MSBTS_Schema`,
übernommen und der Lauf wird fortgesetzt. Schlägt bereits `MSBTS_Map` und `MSBTS_Pipeline` nicht zu den verlässlich registrierten
`MSBTS_GroupSetting` fehl, kann die Management-Datenbank über die beiden 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. `--management-*`-Optionen vorgegeben werden.
### ExplorerOM-Assembly fehlt ### ExplorerOM-Assembly fehlt
@@ -15,8 +15,8 @@ using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Collectors namespace BizTalkSapEnvironmentInventory.Collectors
{ {
/// <summary> /// <summary>
/// Liest die vollständige Anwendungsliste über ExplorerOM und ergänzt sie um /// Liest Anwendungen sowie bereitgestellte Assembly-Artefakte über ExplorerOM
/// optionale Artefakt-, Adapter- und Hostdaten aus dem BizTalk-WMI-Provider. /// und ergänzt sie um die vom BizTalk-WMI-Provider angebotenen Laufzeitdaten.
/// </summary> /// </summary>
internal sealed class BizTalkWmiCollector internal sealed class BizTalkWmiCollector
{ {
@@ -36,11 +36,31 @@ namespace BizTalkSapEnvironmentInventory.Collectors
ConsoleFileLogger logger, ConsoleFileLogger logger,
int timeoutSeconds, int timeoutSeconds,
int maxArtifactDetailsPerType) int maxArtifactDetailsPerType)
: this(
document,
logger,
timeoutSeconds,
maxArtifactDetailsPerType,
true)
{
}
private BizTalkWmiCollector(
InventoryDocument document,
ConsoleFileLogger logger,
int timeoutSeconds,
int maxArtifactDetailsPerType,
bool initializeWmi)
{ {
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));
this.maxArtifactDetailsPerType = Math.Max(100, maxArtifactDetailsPerType); this.maxArtifactDetailsPerType = Math.Max(100, maxArtifactDetailsPerType);
if (!initializeWmi)
{
return;
}
scope = new ManagementScope(WmiNamespace); scope = new ManagementScope(WmiNamespace);
scope.Options.Timeout = timeout; scope.Options.Timeout = timeout;
scope.Options.EnablePrivileges = false; scope.Options.EnablePrivileges = false;
@@ -110,6 +130,8 @@ namespace BizTalkSapEnvironmentInventory.Collectors
logger.Info(applications.Count + " BizTalk-Anwendung(en) geladen (" logger.Info(applications.Count + " BizTalk-Anwendung(en) geladen ("
+ timer.ElapsedMilliseconds + " ms)."); + timer.ElapsedMilliseconds + " ms).");
var explorerArtifactCounts =
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var item in applications) foreach (var item in applications)
{ {
var name = RequiredText(item, "Name"); var name = RequiredText(item, "Name");
@@ -118,6 +140,10 @@ namespace BizTalkSapEnvironmentInventory.Collectors
application.Status = FormatStatus(OptionalText(item, "Status")); application.Status = FormatStatus(OptionalText(item, "Status"));
logger.Detail("ExplorerOM-Anwendung: " + name logger.Detail("ExplorerOM-Anwendung: " + name
+ "; Status=" + application.Status + "."); + "; Status=" + application.Status + ".");
CollectExplorerArtifacts(
item,
application,
explorerArtifactCounts);
} }
if (document.Applications.Count == 0) if (document.Applications.Count == 0)
@@ -125,6 +151,14 @@ namespace BizTalkSapEnvironmentInventory.Collectors
throw new InvalidOperationException( throw new InvalidOperationException(
"Das BizTalk Explorer Object Model lieferte keine Anwendungen."); "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 finally
{ {
@@ -145,11 +179,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors
new ClassDescriptor("MSBTS_SendPort", "SendPort"), new ClassDescriptor("MSBTS_SendPort", "SendPort"),
new ClassDescriptor("MSBTS_SendPortGroup", "SendPortGroup"), new ClassDescriptor("MSBTS_SendPortGroup", "SendPortGroup"),
new ClassDescriptor("MSBTS_ReceivePort", "ReceivePort"), new ClassDescriptor("MSBTS_ReceivePort", "ReceivePort"),
new ClassDescriptor("MSBTS_ReceiveLocation", "ReceiveLocation"), 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")
}; };
foreach (var descriptor in artifactClasses) foreach (var descriptor in artifactClasses)
@@ -165,9 +195,6 @@ namespace BizTalkSapEnvironmentInventory.Collectors
} }
TryCollectComponents("MSBTS_HostSetting", "Host", document.Hosts, false); TryCollectComponents("MSBTS_HostSetting", "Host", document.Hosts, false);
TryCollectComponents("MSBTS_HostInstance", "HostInstance", document.Hosts, false); TryCollectComponents("MSBTS_HostInstance", "HostInstance", document.Hosts, false);
TryCollectWmiSapEndpoints("MSBTS_SendPort", "Senden");
TryCollectWmiSapEndpoints("MSBTS_ReceiveLocation", "Empfangen");
} }
private void LogRuntimeInformation() private void LogRuntimeInformation()
@@ -233,7 +260,9 @@ namespace BizTalkSapEnvironmentInventory.Collectors
+ "Die über ExplorerOM geladene Anwendungsliste bleibt verwendbar.", + "Die über ExplorerOM geladene Anwendungsliste bleibt verwendbar.",
Owner = "BizTalk-Betrieb" Owner = "BizTalk-Betrieb"
}); });
logger.Exception("BizTalk-WMI-Verbindung fehlgeschlagen", exception); logger.WarningException(
"BizTalk-WMI-Verbindung fehlgeschlagen",
exception);
logger.Warning( logger.Warning(
"Optionale WMI-Zusatzdaten werden übersprungen; " "Optionale WMI-Zusatzdaten werden übersprungen; "
+ "die ExplorerOM-Anwendungsliste bleibt erhalten."); + "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++; count++;
} }
@@ -481,27 +528,20 @@ namespace BizTalkSapEnvironmentInventory.Collectors
} }
} }
private void TryCollectWmiSapEndpoints(string className, string direction) private void TryAddWmiSapEndpoint(ManagementObject row, string direction)
{
List<ManagementObject> rows = null;
try
{
rows = Query(
className + " SAP-Fallback",
"SELECT * FROM " + className);
foreach (var row in rows)
{ {
if (!LooksLikeSap(row)) if (!LooksLikeSap(row))
{ {
continue; return;
} }
var endpointName = First(row, "Name"); var endpointName = First(row, "Name");
if (document.SapEndpoints.Any(item => if (string.IsNullOrWhiteSpace(endpointName)
|| document.SapEndpoints.Any(item =>
string.Equals(item.Direction, direction, StringComparison.OrdinalIgnoreCase) string.Equals(item.Direction, direction, StringComparison.OrdinalIgnoreCase)
&& string.Equals(item.Name, endpointName, StringComparison.OrdinalIgnoreCase))) && string.Equals(item.Name, endpointName, StringComparison.OrdinalIgnoreCase)))
{ {
continue; return;
} }
var endpoint = new SapEndpointRecord var endpoint = new SapEndpointRecord
@@ -527,16 +567,6 @@ namespace BizTalkSapEnvironmentInventory.Collectors
AddSelectedProperties(row, endpoint.Properties); AddSelectedProperties(row, endpoint.Properties);
document.SapEndpoints.Add(endpoint); document.SapEndpoints.Add(endpoint);
} }
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
AddOptionalWmiFinding(className + " SAP-Fallback", exception);
}
finally
{
DisposeRows(rows);
}
}
private List<ManagementObject> Query(string name, string query) private List<ManagementObject> Query(string name, string query)
{ {
@@ -591,6 +621,247 @@ namespace BizTalkSapEnvironmentInventory.Collectors
return result; return result;
} }
/// <summary>
/// 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.
/// </summary>
private void CollectExplorerArtifacts(
object applicationObject,
ApplicationRecord application,
IDictionary<string, int> 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<string, int> 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<string, int> 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<object> 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<object>();
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<object>();
}
}
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<string, int>(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) private static string RequiredText(object instance, string propertyName)
{ {
var value = Convert.ToString( var value = Convert.ToString(
@@ -607,9 +878,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors
private string OptionalText(object instance, string propertyName) private string OptionalText(object instance, string propertyName)
{ {
var property = instance.GetType().GetProperty( var property = FindProperty(instance, propertyName);
propertyName,
BindingFlags.Instance | BindingFlags.Public);
if (property == null) if (property == null)
{ {
return string.Empty; return string.Empty;
@@ -621,10 +890,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors
property.GetValue(instance, null), property.GetValue(instance, null),
CultureInfo.InvariantCulture) ?? string.Empty; CultureInfo.InvariantCulture) ?? string.Empty;
} }
catch (Exception exception) when ( catch (Exception exception) when (IsOptionalExplorerException(exception))
exception is TargetInvocationException
|| exception is COMException
|| exception is NotSupportedException)
{ {
logger.Detail("Optionale ExplorerOM-Eigenschaft " logger.Detail("Optionale ExplorerOM-Eigenschaft "
+ instance.GetType().FullName + 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) private static object RequiredProperty(object instance, string propertyName)
{ {
var property = instance.GetType().GetProperty( var property = FindProperty(instance, propertyName);
propertyName,
BindingFlags.Instance | BindingFlags.Public);
if (property == null) if (property == null)
{ {
throw new MissingMemberException(instance.GetType().FullName, propertyName); throw new MissingMemberException(instance.GetType().FullName, propertyName);
@@ -654,9 +932,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors
string propertyName, string propertyName,
object value) object value)
{ {
var property = instance.GetType().GetProperty( var property = FindProperty(instance, propertyName);
propertyName,
BindingFlags.Instance | BindingFlags.Public);
if (property == null || !property.CanWrite) if (property == null || !property.CanWrite)
{ {
throw new MissingMemberException(instance.GetType().FullName, propertyName); throw new MissingMemberException(instance.GetType().FullName, propertyName);
@@ -665,6 +941,23 @@ namespace BizTalkSapEnvironmentInventory.Collectors
property.SetValue(instance, value, null); 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) private ApplicationRecord GetOrCreateApplication(string name)
{ {
var application = document.Applications.FirstOrDefault(item => 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.", RecommendedAction = "Nur relevant, wenn die zugehörige Artefaktspalte vollständig benötigt wird.",
Owner = "BizTalk-Betrieb" Owner = "BizTalk-Betrieb"
}); });
logger.Exception( logger.WarningException(
"Optionale WMI-Abfrage " + className + " fehlgeschlagen", "Optionale WMI-Abfrage " + className + " fehlgeschlagen",
exception); 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) private static bool IsRecoverableWmiException(Exception exception)
{ {
return exception is ManagementException return exception is ManagementException
@@ -165,7 +165,9 @@ namespace BizTalkSapEnvironmentInventory.Collectors
+ "die BizTalk-Erfassung wird fortgesetzt.", + "die BizTalk-Erfassung wird fortgesetzt.",
Owner = "Windows-/BizTalk-Betrieb" 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.", + "--management-Parameter können die Ermittlung übernehmen.",
Owner = "Windows-/BizTalk-Betrieb" Owner = "Windows-/BizTalk-Betrieb"
}); });
logger.Exception( logger.WarningException(
"BizTalk-Registryansicht " + view + " fehlgeschlagen", "BizTalk-Registryansicht " + view + " fehlgeschlagen",
exception); exception);
} }
@@ -330,7 +332,9 @@ namespace BizTalkSapEnvironmentInventory.Collectors
document, document,
"MSBTS_GroupSetting konnte für die Management-DB-Erkennung " "MSBTS_GroupSetting konnte für die Management-DB-Erkennung "
+ "nicht gelesen werden: " + exception.Message); + "nicht gelesen werden: " + exception.Message);
logger.Exception("MSBTS_GroupSetting fehlgeschlagen", exception); logger.WarningException(
"MSBTS_GroupSetting fehlgeschlagen",
exception);
} }
} }
@@ -47,14 +47,45 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
/// WMI-Status und Stacktrace, damit Providerfehler vor Ort analysierbar bleiben. /// WMI-Status und Stacktrace, damit Providerfehler vor Ort analysierbar bleiben.
/// </summary> /// </summary>
public void Exception(string context, Exception exception) public void Exception(string context, Exception exception)
{
WriteException(context, exception, false);
}
/// <summary>
/// Protokolliert eine vollständige Ausnahmekette als Warnung, wenn ein
/// optionaler oder durch einen Fallback abgedeckter Zugriff fehlschlägt.
/// </summary>
public void WarningException(string context, Exception exception)
{
WriteException(context, exception, true);
}
private void WriteException(
string context,
Exception exception,
bool warning)
{ {
if (exception == null) if (exception == null)
{
if (warning)
{
Warning(context + ": Unbekannter Fehler ohne Ausnahmeobjekt.");
}
else
{ {
Error(context + ": Unbekannter Fehler ohne Ausnahmeobjekt."); Error(context + ": Unbekannter Fehler ohne Ausnahmeobjekt.");
}
return; return;
} }
if (warning)
{
Warning(context + ": " + exception.Message);
}
else
{
Error(context + ": " + exception.Message); Error(context + ": " + exception.Message);
}
var current = exception; var current = exception;
var depth = 0; var depth = 0;
while (current != null) while (current != null)
@@ -63,8 +63,15 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
+ "und lokale BizTalk-Konfiguration prüfen.", + "und lokale BizTalk-Konfiguration prüfen.",
Owner = "BizTalk-Betrieb" Owner = "BizTalk-Betrieb"
}); });
if (required)
{
logger.Exception(name, exception); logger.Exception(name, exception);
} }
else
{
logger.WarningException(name, exception);
}
}
} }
} }
} }
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
@@ -24,6 +25,7 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
{ {
TestSanitizer(); TestSanitizer();
TestBindingParser(); TestBindingParser();
TestExplorerArtifactReader();
TestLoggerDiagnostics(); TestLoggerDiagnostics();
TestDocxPackage(); 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() private static void TestLoggerDiagnostics()
{ {
var directory = CreateTemporaryDirectory(); var directory = CreateTemporaryDirectory();
@@ -254,7 +303,7 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
// Die absichtlich erzeugte Testausnahme soll den Self-Test // Die absichtlich erzeugte Testausnahme soll den Self-Test
// auf der Konsole nicht wie einen echten Laufzeitfehler aussehen lassen. // auf der Konsole nicht wie einen echten Laufzeitfehler aussehen lassen.
Console.SetOut(TextWriter.Null); Console.SetOut(TextWriter.Null);
logger.Exception("Self-Test-Diagnose", exception); logger.WarningException("Self-Test-Diagnose", exception);
} }
finally finally
{ {
@@ -272,6 +321,10 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
Assert( Assert(
log.Contains("HRESULT=0x"), log.Contains("HRESULT=0x"),
"Logger dokumentierte den HRESULT nicht."); "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 finally
{ {
@@ -381,5 +434,79 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
throw new InvalidOperationException(message); 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; }
}
} }
} }
@@ -83,8 +83,9 @@ namespace BizTalkSapEnvironmentInventory
logger.Info("Server: " + Environment.MachineName); logger.Info("Server: " + Environment.MachineName);
logger.Info("Ausgabeordner: " + options.OutputDirectory); logger.Info("Ausgabeordner: " + options.OutputDirectory);
logger.Info( logger.Info(
"Modus: read-only; ExplorerOM für Anwendungen, WMI für " "Modus: read-only; ExplorerOM für Anwendungen und "
+ "Gruppenermittlung und optionale Artefakte."); + "bereitgestellte Assembly-Artefakte; WMI für "
+ "Gruppenermittlung und Laufzeitobjekte.");
logger.Detail("Logdatei: " + logPath); logger.Detail("Logdatei: " + logPath);
logger.Detail("DOCX-Zieldatei: " + reportPath); logger.Detail("DOCX-Zieldatei: " + reportPath);
@@ -105,7 +106,7 @@ namespace BizTalkSapEnvironmentInventory
logger, logger,
wmiTimeoutSeconds).Collect(document)); wmiTimeoutSeconds).Collect(document));
safeCollector.Execute( safeCollector.Execute(
"BizTalk-Anwendungen und optionale WMI-Artefakte", "BizTalk-Anwendungen und -Artefakte",
true, true,
() => new BizTalkWmiCollector( () => new BizTalkWmiCollector(
document, document,
@@ -168,9 +168,10 @@ namespace BizTalkSapEnvironmentInventory.Reporting
WriteParagraph( WriteParagraph(
writer, writer,
"Die vollständige Anwendungsliste stammt aus dem read-only BizTalk Explorer " "Die vollständige Anwendungsliste stammt aus dem read-only BizTalk Explorer "
+ "Object Model. Artefaktzahlen werden aus lokal verfügbaren, optionalen " + "Object Model. Assemblies, Schemas, Maps und Pipelines werden ebenfalls "
+ "BizTalk-WMI-Klassen gebildet; fehlende Klassen werden im Log und als " + "über ExplorerOM gelesen. Laufzeitobjekte wie Ports und Orchestrierungen "
+ "Finding ausgewiesen, ohne die Anwendungsliste zu verwerfen.", + "stammen aus den lokal verfügbaren BizTalk-WMI-Klassen. Fehler optionaler "
+ "Zusatzdaten verwerfen die Anwendungsliste nicht.",
"Normal", "Normal",
false, false,
null); null);