Support ScheduledTask control and persistent logs
This commit is contained in:
+4
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Projekt und Sicherheitsziel
|
## Projekt und Sicherheitsziel
|
||||||
|
|
||||||
Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.2.4.
|
Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.3.0.
|
||||||
|
|
||||||
## Installerinvarianten
|
## Installerinvarianten
|
||||||
|
|
||||||
@@ -35,6 +35,9 @@ Die zentrale Implementierung liegt in `src/BizTalkPlatformManagementTool.Setup/I
|
|||||||
- Der echte Emergency Restore erzeugt nach Möglichkeit automatisch einen timestamp-basierten Soll/Ist-Diff aus Recovery-Quelle und Nachher-Snapshot.
|
- Der echte Emergency Restore erzeugt nach Möglichkeit automatisch einen timestamp-basierten Soll/Ist-Diff aus Recovery-Quelle und Nachher-Snapshot.
|
||||||
- Die zentrale best-effort Orchestrierung liegt in `OperationPlanExecutor`; die WMI-/Service-Zustandsprüfung bleibt im produktiven Runtime-Adapter.
|
- Die zentrale best-effort Orchestrierung liegt in `OperationPlanExecutor`; die WMI-/Service-Zustandsprüfung bleibt im produktiven Runtime-Adapter.
|
||||||
- Datei-Logging und optionale GUI-Logweiterleitung sind Diagnosekanäle und dürfen niemals einen fachlichen Planschritt oder dessen Fehlerbehandlung unterbrechen.
|
- Datei-Logging und optionale GUI-Logweiterleitung sind Diagnosekanäle und dürfen niemals einen fachlichen Planschritt oder dessen Fehlerbehandlung unterbrechen.
|
||||||
|
- Scheduler-Preflight darf nur für Receive Locations mit passendem Adaptername oder `scheduler:`-URI laufen; normale Adapter dürfen keine zusätzliche Abhängigkeit erhalten.
|
||||||
|
- Adapter-DLLs dürfen nur aus explizit konfigurierten, per Registry erkannten oder konventionellen Produktverzeichnissen und nur nach vollständiger Assemblyidentitätsprüfung geladen werden. Der Toolprozess verändert niemals den GAC.
|
||||||
|
- Laufzeitlogs halten 30 Kalendertage, komprimieren abgeschlossene Tage und rehydrieren maximal 10.000 neue Einträge ins Grid. Ein Archiv-/Lesefehler darf keine BizTalk-Operation beeinflussen.
|
||||||
|
|
||||||
## Versionierung
|
## Versionierung
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [2.3.0] - 2026-08-26
|
||||||
|
### Added
|
||||||
|
- Process-local, assembly-identity-checked resolution and preflight for ScheduledTask `scheduler:` receive locations, including automatic BizTalk/adapter directory and registry discovery plus an explicit configuration override.
|
||||||
|
- Persistent Operation Log grid restoration from retained plain and GZip-compressed daily logs.
|
||||||
|
- **Log Folder** action and a dedicated PROD validation/support runbook in Markdown and plain text.
|
||||||
|
- Regression coverage for Scheduler plan metadata, assembly identity rejection, restart persistence, compression, 30-day retention and detailed exception diagnostics.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Runtime logs now retain 30 calendar days and compress completed daily files instead of deleting everything older than five days.
|
||||||
|
- Grid, result JSON and file logs now preserve exception type, HRESULT, inner exceptions, Fusion loader details and stack traces for support analysis.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- ScheduledTask 7.0.2 receive locations can resolve `Microsoft.BizTalk.Scheduler.dll` from the installed BizTalk environment before WMI `Enable`/`Disable`, without requiring the management tool to mutate the GAC.
|
||||||
|
|
||||||
## [2.2.4] - 2026-08-24
|
## [2.2.4] - 2026-08-24
|
||||||
### Fixed
|
### Fixed
|
||||||
- Permission, Windows Script Host, validation and rollback errors affecting the optional all-users desktop shortcut no longer abort installation, update or uninstallation.
|
- Permission, Windows Script Host, validation and rollback errors affecting the optional all-users desktop shortcut no longer abort installation, update or uninstallation.
|
||||||
|
|||||||
+22
-4
@@ -1,6 +1,6 @@
|
|||||||
# BizTalk Platform Management Tool Dokumentation
|
# BizTalk Platform Management Tool Dokumentation
|
||||||
|
|
||||||
**Stand:** 2026-08-19
|
**Stand:** 2026-08-26
|
||||||
**Implementierung:** C# WinForms, .NET Framework 4.6.1
|
**Implementierung:** C# WinForms, .NET Framework 4.6.1
|
||||||
**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster
|
|||||||
- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
|
- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
|
||||||
- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs`
|
- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs`
|
||||||
- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs`
|
- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs`
|
||||||
|
- Adapter-Assemblyauflösung: `src/BizTalkPlatformManagementTool/Services/AdapterAssemblyResolver.cs`
|
||||||
|
- Persistentes Laufzeitlogging: `src/BizTalkPlatformManagementTool/Services/OperationLogger.cs`
|
||||||
- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs`
|
- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs`
|
||||||
- Snapshot-Validierung: `src/BizTalkPlatformManagementTool/Services/SnapshotValidator.cs`
|
- Snapshot-Validierung: `src/BizTalkPlatformManagementTool/Services/SnapshotValidator.cs`
|
||||||
- Installer: `src/BizTalkPlatformManagementTool.Setup`
|
- Installer: `src/BizTalkPlatformManagementTool.Setup`
|
||||||
@@ -58,7 +60,7 @@ Selbsterklärende Zuweisungen und reine UI-Konstruktion werden nicht zeilenweise
|
|||||||
|
|
||||||
Wenn ein echter Shutdown oder Restore nur teilweise ausgeführt wurde und lediglich die ursprüngliche `before.json` verfügbar ist, wird **Emergency Restore** verwendet. Die Quelle kann über **State...** ausgewählt und vorab über **Validate State** ohne WMI-Zugriff geprüft werden. Der Modus kopiert die Quelle unter einem timestamp-basierten Namen, überschreibt `before.json` nicht, stellt zuerst `ENTSSO` sicher und gleicht danach den gespeicherten Sollzustand zustandsbewusst ab. Der erste Lauf muss als Dry-run erfolgen.
|
Wenn ein echter Shutdown oder Restore nur teilweise ausgeführt wurde und lediglich die ursprüngliche `before.json` verfügbar ist, wird **Emergency Restore** verwendet. Die Quelle kann über **State...** ausgewählt und vorab über **Validate State** ohne WMI-Zugriff geprüft werden. Der Modus kopiert die Quelle unter einem timestamp-basierten Namen, überschreibt `before.json` nicht, stellt zuerst `ENTSSO` sicher und gleicht danach den gespeicherten Sollzustand zustandsbewusst ab. Der erste Lauf muss als Dry-run erfolgen.
|
||||||
|
|
||||||
Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des letzten Snapshots und zeigt `Started`, `Stopped`, `Partial` oder `Unknown`. **Clear** leert die sichtbaren Ergebnis- und Log-Grids, loescht aber keine Dateien. **Close** beendet die Anwendung.
|
Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des letzten Snapshots und zeigt `Started`, `Stopped`, `Partial` oder `Unknown`. **Clear** leert die sichtbaren Ergebnis- und Log-Grids, loescht aber keine Dateien. Beim nächsten Start wird die aufbewahrte Historie erneut geladen. **Log Folder** öffnet den lokalen Ablageordner, **Close** beendet die Anwendung.
|
||||||
|
|
||||||
## Sicherheitsdesign
|
## Sicherheitsdesign
|
||||||
|
|
||||||
@@ -67,7 +69,8 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
|
|||||||
- Echte Shutdown-/Restore-Aktionen verlangen erst nach Erzeugung und Speicherung des frischen Plans eine zusätzliche Bestätigung mit Zielserver, Plandatei und exakter Zahl ausführbarer Schritte.
|
- Echte Shutdown-/Restore-Aktionen verlangen erst nach Erzeugung und Speicherung des frischen Plans eine zusätzliche Bestätigung mit Zielserver, Plandatei und exakter Zahl ausführbarer Schritte.
|
||||||
- Jede Operation schreibt Einträge in das sichtbare Operation Log.
|
- Jede Operation schreibt Einträge in das sichtbare Operation Log.
|
||||||
- Zusätzlich wird unter `%ProgramData%\BizTalkPlatformManagementTool\Logs` eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. Nur wenn ProgramData nicht verfügbar ist, wird auf das EXE-Verzeichnis zurückgefallen.
|
- Zusätzlich wird unter `%ProgramData%\BizTalkPlatformManagementTool\Logs` eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. Nur wenn ProgramData nicht verfügbar ist, wird auf das EXE-Verzeichnis zurückgefallen.
|
||||||
- Logdateien werden rollierend für den aktuellen Tag plus vier vorherige Tage vorgehalten.
|
- Der aktuelle Tag bleibt als `.log` lesbar. Abgeschlossene Vortage werden als `.log.gz` komprimiert; aktueller Tag plus 29 Vortage bleiben erhalten.
|
||||||
|
- Beim Programmstart werden bis zu 10.000 der neuesten Einträge aus `.log` und `.log.gz` chronologisch in das Operation Log geladen.
|
||||||
- Der Kopfbereich zeigt den zuletzt erkannten Umgebungsstatus aus den Host Instances.
|
- Der Kopfbereich zeigt den zuletzt erkannten Umgebungsstatus aus den Host Instances.
|
||||||
- Operationspläne werden vor Laufzeitänderungen gespeichert.
|
- Operationspläne werden vor Laufzeitänderungen gespeichert.
|
||||||
- Jeder ausführbare Schritt prüft vor der Mutation, ob sein Sollzustand bereits erreicht ist.
|
- Jeder ausführbare Schritt prüft vor der Mutation, ob sein Sollzustand bereits erreicht ist.
|
||||||
@@ -78,6 +81,7 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
|
|||||||
- Wartezeiten nutzen konfigurierbare Timeout- und Polling-Werte.
|
- Wartezeiten nutzen konfigurierbare Timeout- und Polling-Werte.
|
||||||
- Host Instances auf anderen Servern werden übersprungen und als Warnung protokolliert.
|
- Host Instances auf anderen Servern werden übersprungen und als Warnung protokolliert.
|
||||||
- Echte Shutdown-/Restore-Schritte protokollieren WMI-Klasse, Schlüssel, Zielobjekt und Methode, damit Fehler wie WMI-Query- oder Methodenfehler eindeutig zugeordnet werden können.
|
- Echte Shutdown-/Restore-Schritte protokollieren WMI-Klasse, Schlüssel, Zielobjekt und Methode, damit Fehler wie WMI-Query- oder Methodenfehler eindeutig zugeordnet werden können.
|
||||||
|
- Fehler protokollieren Typ, HRESULT, vollständige innere Exception-Kette, vorhandenes Fusion-Loaderprotokoll und Stacktrace identisch im Grid, Ergebnisreport und Laufzeitlog.
|
||||||
- Restore-Pläne werden abgelehnt, wenn Snapshot-Server und ausgewählter Zielserver nicht übereinstimmen; Kurzname und FQDN desselben Hosts gelten als identisch.
|
- Restore-Pläne werden abgelehnt, wenn Snapshot-Server und ausgewählter Zielserver nicht übereinstimmen; Kurzname und FQDN desselben Hosts gelten als identisch.
|
||||||
- Pro Windows-Sitzung ist nur eine Toolinstanz zulässig; das Fenster kann während einer aktiven WMI-Operation nicht geschlossen werden.
|
- Pro Windows-Sitzung ist nur eine Toolinstanz zulässig; das Fenster kann während einer aktiven WMI-Operation nicht geschlossen werden.
|
||||||
- Die EXE enthält zusätzlich einen WMI-freien `--self-test`, den der Installer vor und nach der Aktivierung ausführt.
|
- Die EXE enthält zusätzlich einen WMI-freien `--self-test`, den der Installer vor und nach der Aktivierung ausführt.
|
||||||
@@ -120,6 +124,18 @@ Der Best-effort-Ansatz bedeutet nicht, dass Fehler ignoriert werden: Jeder Teilf
|
|||||||
- Orchestrations mit Status `Bound` bleiben unverändert und werden zur manuellen Prüfung markiert, weil ein blindes Unenlist zu `Unbound` führen kann.
|
- Orchestrations mit Status `Bound` bleiben unverändert und werden zur manuellen Prüfung markiert, weil ein blindes Unenlist zu `Unbound` führen kann.
|
||||||
- Receive Locations werden zuletzt aktiviert, damit eingehender Verkehr erst nach Host Instances und abhängigen Artefakten wieder aufgenommen wird.
|
- Receive Locations werden zuletzt aktiviert, damit eingehender Verkehr erst nach Host Instances und abhängigen Artefakten wieder aufgenommen wird.
|
||||||
|
|
||||||
|
## ScheduledTask-Adapter
|
||||||
|
|
||||||
|
Beim Fremdadapter `Schedule`/ScheduledTask 7.0.2 wird `MSBTS_ReceiveLocation.Enable` beziehungsweise `.Disable` erst ausgeführt, nachdem die für die Transportdatenvalidierung erforderliche `Microsoft.BizTalk.Scheduler.dll` prozesslokal geladen werden konnte. Die Planzeile enthält dafür Adaptername und `scheduler:`-Adresse aus dem Snapshot.
|
||||||
|
|
||||||
|
Der Resolver sucht nur in vorhandenen, vertrauenswürdigen Installationsverzeichnissen und akzeptiert eine DLL erst nach Prüfung der Assemblyidentität. Er berücksichtigt:
|
||||||
|
|
||||||
|
1. `AdapterAssemblySearchPaths` aus der EXE-Konfiguration,
|
||||||
|
2. Installations- und Assemblypfade aus beiden BizTalk-Registryansichten,
|
||||||
|
3. lokale Verzeichnisse `Microsoft BizTalk Server*` und `BizTalk ScheduledTask Adapter*` unter Program Files (x86).
|
||||||
|
|
||||||
|
Die Standardkonfiguration enthält BizTalk Server 2020 und den im PROD-Screenshot bestätigten Pfad `C:\Program Files (x86)\BizTalk ScheduledTask Adapter 7.0.2`. Die Auflösung gilt nur im Toolprozess. Es werden weder GAC noch Adapterdateien verändert. Ist die erforderliche Assembly nicht auffindbar oder nicht zur angeforderten Identität passend, scheitert nur dieser Schritt mit vollständiger Pfad-, Versions- und Loaderdiagnose; der Executor arbeitet die restlichen unabhängigen Schritte weiter ab.
|
||||||
|
|
||||||
## Ausgabedateien
|
## Ausgabedateien
|
||||||
|
|
||||||
- Snapshots: `before.json`, `after.json`
|
- Snapshots: `before.json`, `after.json`
|
||||||
@@ -129,13 +145,15 @@ Der Best-effort-Ansatz bedeutet nicht, dass Fehler ignoriert werden: Jeder Teilf
|
|||||||
- Emergency-Dateien: `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*`
|
- Emergency-Dateien: `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*`
|
||||||
- Diff: `diff.json`, `diff.csv`, `diff.html`
|
- Diff: `diff.json`, `diff.csv`, `diff.html`
|
||||||
- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html`
|
- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html`
|
||||||
- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs\BizTalkPlatformManagementTool-yyyy-MM-dd.log`
|
- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs\BizTalkPlatformManagementTool-yyyy-MM-dd.log` und komprimierte Vortage `*.log.gz`
|
||||||
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs\setup-*.log`
|
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs\setup-*.log`
|
||||||
|
|
||||||
## Fehleranalyse
|
## Fehleranalyse
|
||||||
|
|
||||||
Bei echten Shutdown- und Restore-Aktionen wird jeder Schritt vor der Ausführung mit Artefakttyp, WMI-Klasse, Schlüsselproperty, Schlüsselwert und Methodenname protokolliert. Die Objektauflösung verwendet eine breite `SELECT * FROM <class>`-Abfrage und filtert danach im Prozess auf den Schlüsselwert. Dadurch können Host-Instance-Namen und andere BizTalk-Namen mit Sonderzeichen keine ungültige WMI-WQL-`WHERE`-Query mehr erzeugen.
|
Bei echten Shutdown- und Restore-Aktionen wird jeder Schritt vor der Ausführung mit Artefakttyp, WMI-Klasse, Schlüsselproperty, Schlüsselwert und Methodenname protokolliert. Die Objektauflösung verwendet eine breite `SELECT * FROM <class>`-Abfrage und filtert danach im Prozess auf den Schlüsselwert. Dadurch können Host-Instance-Namen und andere BizTalk-Namen mit Sonderzeichen keine ungültige WMI-WQL-`WHERE`-Query mehr erzeugen.
|
||||||
|
|
||||||
|
Bei `scheduler:` erscheinen zusätzlich `ScheduledTask adapter preflight`, alle tatsächlich verwendeten Suchverzeichnisse, Prozessbitness und bei Erfolg die vollständige geladene Assemblyidentität samt Pfad. Ein Ladefehler enthält die anfordernde Assembly, HRESULT, Fusion-Details und Stacktrace. Für eine Supportanalyse sind die Tagesdatei, relevante `.log.gz`, der passende `*-result.json`, Plan, Vorher-/Nachher-Snapshot und die unveränderte EXE-Konfiguration zu sichern.
|
||||||
|
|
||||||
Snapshot- und Plan-JSON-Dateien werden als UTF-8 ohne BOM über eine temporäre Datei im Zielverzeichnis und anschließenden atomaren Austausch geschrieben. Beim Laden werden vorhandene Dateien mit UTF-8-BOM oder durch Encoding-Konvertierung sichtbar gewordenem BOM-Marker toleriert. Deserialisierte Snapshots werden normalisiert und auf leere Namen, Duplikate und fehlende Strukturen geprüft.
|
Snapshot- und Plan-JSON-Dateien werden als UTF-8 ohne BOM über eine temporäre Datei im Zielverzeichnis und anschließenden atomaren Austausch geschrieben. Beim Laden werden vorhandene Dateien mit UTF-8-BOM oder durch Encoding-Konvertierung sichtbar gewordenem BOM-Marker toleriert. Deserialisierte Snapshots werden normalisiert und auf leere Namen, Duplikate und fehlende Strukturen geprüft.
|
||||||
|
|
||||||
Diffs verwenden den zusammengesetzten Schlüssel aus Anwendung und Artefaktname. Gleichnamige Artefakte in verschiedenen BizTalk-Anwendungen überschreiben sich daher nicht mehr. CSV-Werte mit Präfix `=`, `+`, `-`, `@` oder Tab werden mit einem Apostroph neutralisiert, damit Tabellenkalkulationen sie nicht als Formel ausführen.
|
Diffs verwenden den zusammengesetzten Schlüssel aus Anwendung und Artefaktname. Gleichnamige Artefakte in verschiedenen BizTalk-Anwendungen überschreiben sich daher nicht mehr. CSV-Werte mit Präfix `=`, `+`, `-`, `@` oder Tab werden mit einem Apostroph neutralisiert, damit Tabellenkalkulationen sie nicht als Formel ausführen.
|
||||||
|
|||||||
+3
-1
@@ -99,7 +99,9 @@ Ein erfolgreicher Installer-Self-Test bestätigt Paket, Programmstart und lokale
|
|||||||
|
|
||||||
## Emergency Restore nach einem Teilabbruch
|
## Emergency Restore nach einem Teilabbruch
|
||||||
|
|
||||||
Version 2.2.4 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Auch ein Fehler der GUI-Logweiterleitung ist vom fachlichen Executor isoliert und kann die späteren Planschritte nicht abbrechen.
|
Version 2.3.0 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Auch ein Fehler der GUI-Logweiterleitung ist vom fachlichen Executor isoliert und kann die späteren Planschritte nicht abbrechen.
|
||||||
|
|
||||||
|
Für ScheduledTask-Receive-Locations enthält die installierte EXE-Konfiguration `AdapterAssemblySearchPaths`. Normalerweise ist keine Anpassung nötig: BizTalk- und ScheduledTask-Installationsverzeichnisse werden zusätzlich automatisch erkannt. Nur bei einem abweichenden lokalen Installationspfad darf der vorhandene Wert um diesen Pfad ergänzt werden. Die Assembly muss aus genau dieser BizTalk-/Adapterinstallation stammen; das Tool verändert den GAC nicht.
|
||||||
|
|
||||||
1. Die erhaltene `before.json` außerhalb des Arbeitsverzeichnisses zusätzlich sichern.
|
1. Die erhaltene `before.json` außerhalb des Arbeitsverzeichnisses zusätzlich sichern.
|
||||||
2. Anwendung als Administrator starten und denselben Zielserver wählen, der im Snapshot gespeichert ist.
|
2. Anwendung als Administrator starten und denselben Zielserver wählen, der im Snapshot gespeichert ist.
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
PROD-RUNBOOK: SCHEDULEDTASK-STEUERUNG UND PERSISTENTES LAUFZEITLOGGING
|
||||||
|
Stand: 2026-08-26
|
||||||
|
Zielversion: 2.3.0
|
||||||
|
Adapter: BizTalk ScheduledTask Adapter 7.0.2
|
||||||
|
Adapterpfad: C:\Program Files (x86)\BizTalk ScheduledTask Adapter 7.0.2
|
||||||
|
|
||||||
|
URSACHE
|
||||||
|
MSBTS_ReceiveLocation.Enable/Disable validiert die Transportdaten. Der Adapter
|
||||||
|
benoetigt dabei Microsoft.BizTalk.Scheduler.dll (beobachtet: Version 3.13.0.0).
|
||||||
|
Die Datei liegt in der BizTalk-Installation, ist aber nicht zwingend im GAC
|
||||||
|
aufloesbar. Version 2.3.0 laedt sie nach Assemblyidentitaetspruefung nur in den
|
||||||
|
laufenden Toolprozess. Das Tool kopiert keine DLL und aendert den GAC nicht.
|
||||||
|
|
||||||
|
VORBEREITUNG
|
||||||
|
1. before.json, Reports und vorhandene Logs extern sichern.
|
||||||
|
2. Setup 2.3.0 als Administrator installieren/aktualisieren.
|
||||||
|
3. setup_version=2.3.0.0 und erfolgreichen Ziel-Self-Test pruefen.
|
||||||
|
4. Tool als Administrator starten, Log Folder oeffnen.
|
||||||
|
5. Im lokalen BizTalk-Ordner Microsoft.BizTalk.Scheduler.dll pruefen.
|
||||||
|
6. Nur bei Sonderpfad AdapterAssemblySearchPaths in der EXE.config erweitern.
|
||||||
|
Mehrere Pfade mit Semikolon trennen. Keine fremde/alte DLL kopieren.
|
||||||
|
|
||||||
|
FUNKTIONSTEST
|
||||||
|
1. Diagnose.
|
||||||
|
2. Snapshot Before; Scheduler-Receive-Location und scheduler:-URI pruefen.
|
||||||
|
3. Shutdown zuerst mit Dry run; shutdown-plan.json reviewen.
|
||||||
|
4. Dry run deaktivieren, Shutdown bestaetigen.
|
||||||
|
5. Erwartete Logzeilen:
|
||||||
|
ScheduledTask adapter preflight
|
||||||
|
ProcessBitness=...; SearchDirectories=...
|
||||||
|
ScheduledTask dependency loaded process-locally / resolved through CLR/GAC
|
||||||
|
(oder already loaded)
|
||||||
|
Calling MSBTS_ReceiveLocation.Disable
|
||||||
|
Reached: Disable receive location completed
|
||||||
|
6. shutdown-result.json: Succeeded oder AlreadySatisfied.
|
||||||
|
7. Restore zuerst Dry run, dann real.
|
||||||
|
8. Calling MSBTS_ReceiveLocation.Enable und Reached pruefen.
|
||||||
|
9. Snapshot After und Compare; Sollzustand muss wiederhergestellt sein.
|
||||||
|
|
||||||
|
LOGGING-TEST
|
||||||
|
1. Lauf mit erkennbarer Uhrzeit erzeugen, Tool schliessen und neu starten.
|
||||||
|
2. Vorherige Eintraege muessen mit Datum/Uhrzeit wieder im Grid stehen.
|
||||||
|
3. Clear leert nur das Grid; Neustart laedt die Historie erneut.
|
||||||
|
4. Aktueller Tag: BizTalkPlatformManagementTool-yyyy-MM-dd.log
|
||||||
|
5. Abgeschlossene Vortage: .log.gz
|
||||||
|
6. Aufbewahrung: aktueller Tag plus 29 Vortage.
|
||||||
|
7. Grid: neueste maximal 10.000 Eintraege; Dateien bleiben vollstaendig.
|
||||||
|
|
||||||
|
FEHLERFALL SICHERN
|
||||||
|
1. Aktuelle .log und relevante .log.gz aus Log Folder.
|
||||||
|
2. shutdown-plan.json oder restore-plan.json.
|
||||||
|
3. Passenden *-result.json-Report.
|
||||||
|
4. before.json und vorhandenen Nachher-Snapshot.
|
||||||
|
5. Unveraenderte BizTalkPlatformManagementTool.exe.config.
|
||||||
|
6. Screenshot des vollstaendigen roten Grid-Eintrags.
|
||||||
|
7. Dateiversion von Microsoft.BizTalk.Scheduler.dll.
|
||||||
|
|
||||||
|
Fehler enthalten Typ, HRESULT, innere Ausnahmen, Fusion-Informationen,
|
||||||
|
Stacktrace, angeforderte/anfragende Assembly und alle Suchpfade. GAC oder DLLs
|
||||||
|
im Fehlerfall nicht spontan veraendern; zuerst dieses Supportpaket auswerten.
|
||||||
|
|
||||||
|
ABNAHME
|
||||||
|
- Scheduler-Receive-Location real deaktiviert und wieder aktiviert.
|
||||||
|
- Normale Adapter ohne Scheduler-Preflight weiterhin funktionsfaehig.
|
||||||
|
- Teilfehler bleibt Failed, spaetere unabhaengige Schritte laufen weiter.
|
||||||
|
- Historie erscheint nach Neustart im Grid.
|
||||||
|
- Vortage komprimiert, 30 Kalendertage aufbewahrt.
|
||||||
|
- Setup/Tool/Reports zeigen 2.3.0 bzw. 2.3.0-net461.
|
||||||
|
|
||||||
|
Detailfassung:
|
||||||
|
docs\PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
|
||||||
@@ -24,6 +24,9 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
|
|||||||
- WMI access through `root\MicrosoftBizTalkServer`
|
- WMI access through `root\MicrosoftBizTalkServer`
|
||||||
- Startup check for administrator rights
|
- Startup check for administrator rights
|
||||||
- Detailed operation logging in the GUI and daily rolling log files under ProgramData
|
- Detailed operation logging in the GUI and daily rolling log files under ProgramData
|
||||||
|
- Automatic operation-log restoration into the grid after an application restart
|
||||||
|
- Thirty-day runtime-log retention with GZip compression for completed daily logs
|
||||||
|
- Process-local ScheduledTask adapter dependency resolution for `scheduler:` receive locations without modifying the GAC
|
||||||
- Best-effort plan execution: one isolated WMI failure is recorded while remaining independent steps continue
|
- Best-effort plan execution: one isolated WMI failure is recorded while remaining independent steps continue
|
||||||
- Idempotent execution that skips artifacts already in the requested target state
|
- Idempotent execution that skips artifacts already in the requested target state
|
||||||
- Durable per-step result reports even when a shutdown or restore completes only partially
|
- Durable per-step result reports even when a shutdown or restore completes only partially
|
||||||
@@ -49,7 +52,7 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
|
|||||||
|
|
||||||
If a shutdown was interrupted and only the original `before.json` remains, select that file with **State...**, run **Validate State**, keep **Dry run** enabled and click **Emergency Restore**. The recovery plan never overwrites the source snapshot, ensures the `ENTSSO` service is running first, skips already-correct runtime states and continues after isolated step failures. Disable Dry run only after reviewing the timestamped emergency plan. A real run automatically writes a timestamped target/actual diff when the post-operation snapshot succeeds.
|
If a shutdown was interrupted and only the original `before.json` remains, select that file with **State...**, run **Validate State**, keep **Dry run** enabled and click **Emergency Restore**. The recovery plan never overwrites the source snapshot, ensures the `ENTSSO` service is running first, skips already-correct runtime states and continues after isolated step failures. Disable Dry run only after reviewing the timestamped emergency plan. A real run automatically writes a timestamped target/actual diff when the post-operation snapshot succeeds.
|
||||||
|
|
||||||
The environment indicator shows `Started`, `Stopped`, `Partial` or `Unknown` from the most recent snapshot. `Clear` removes the visible status and operation log grids; it does not delete files.
|
The environment indicator shows `Started`, `Stopped`, `Partial` or `Unknown` from the most recent snapshot. `Clear` removes the visible status and operation log grids; it does not delete files. **Log Folder** opens the persistent local runtime-log directory.
|
||||||
|
|
||||||
The application requests administrator rights through its UAC manifest and checks them again during startup. Only one GUI instance can run per Windows session.
|
The application requests administrator rights through its UAC manifest and checks them again during startup. Only one GUI instance can run per Windows session.
|
||||||
|
|
||||||
@@ -82,7 +85,7 @@ Orchestrations that were `Bound` are deliberately left unchanged during restore
|
|||||||
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
|
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
|
||||||
- Runtime logs under `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
- Runtime logs under `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||||
|
|
||||||
Log files are retained for the current day plus the previous four days. Older `BizTalkPlatformManagementTool-*.log` files are removed on startup. If ProgramData is unexpectedly unavailable, logging falls back to the executable directory.
|
The current day remains a plain `BizTalkPlatformManagementTool-yyyy-MM-dd.log`. Completed daily logs are compressed to `.log.gz`; the current day plus the previous 29 calendar days are retained. Older records are removed on startup. Up to the newest 10,000 retained entries are automatically restored from plain and compressed files into the Operation Log grid after a restart. If ProgramData is unexpectedly unavailable, logging falls back to the executable directory.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@@ -90,6 +93,10 @@ The Operation Log shows the WMI class, key property, key value and method for re
|
|||||||
|
|
||||||
Execution is deliberately best-effort. A failure such as an adapter-specific validation exception is written as `Failed` in the result report, but later independent plan steps are still attempted. The GUI ends in a failed/operator-review state when any step failed; it never reports a partial execution as an unconditional success. The post-operation snapshot is attempted independently and its own failure is preserved in the same report.
|
Execution is deliberately best-effort. A failure such as an adapter-specific validation exception is written as `Failed` in the result report, but later independent plan steps are still attempted. The GUI ends in a failed/operator-review state when any step failed; it never reports a partial execution as an unconditional success. The post-operation snapshot is attempted independently and its own failure is preserved in the same report.
|
||||||
|
|
||||||
|
For a ScheduledTask receive location, the operation plan retains adapter name and address. Immediately before a real `Enable` or `Disable`, the tool preloads `Microsoft.BizTalk.Scheduler.dll` from the locally installed BizTalk directory and resolves further dependencies from identity-checked BizTalk/ScheduledTask adapter directories. Conventional BizTalk 2020 and `BizTalk ScheduledTask Adapter 7.x` folders plus BizTalk registry paths are discovered automatically. An exceptional installation path can be added to the semicolon-delimited `AdapterAssemblySearchPaths` value in `BizTalkPlatformManagementTool.exe.config`. This is process-local: the tool neither copies DLLs nor changes the GAC. Never point the setting at assemblies from a different BizTalk version.
|
||||||
|
|
||||||
|
Error records in the grid, execution report and file log include exception type, HRESULT, complete inner-exception chain, available Fusion loader information and stack trace. For the PROD validation and support bundle, follow [ScheduledTask control and runtime logging runbook](docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md).
|
||||||
|
|
||||||
Snapshot and plan JSON files are written as UTF-8 without BOM. Loading is tolerant of existing files that contain a UTF-8 BOM or a visible BOM marker from previous encoding conversions.
|
Snapshot and plan JSON files are written as UTF-8 without BOM. Loading is tolerant of existing files that contain a UTF-8 BOM or a visible BOM marker from previous encoding conversions.
|
||||||
|
|
||||||
JSON snapshots and plans are written through a same-directory temporary file and atomic replacement. Snapshot comparison keys artifacts by application plus name, preventing collisions between equal artifact names in different applications. CSV fields that could be interpreted as spreadsheet formulas are neutralized.
|
JSON snapshots and plans are written through a same-directory temporary file and atomic replacement. Snapshot comparison keys artifacts by application plus name, preventing collisions between equal artifact names in different applications. CSV fields that could be interpreted as spreadsheet formulas are neutralized.
|
||||||
@@ -119,5 +126,6 @@ Targeted German inline comments explain non-obvious operational decisions such a
|
|||||||
- [PROD activation incident analysis and 2.2.3 fix](docs/PROD-Installer-Aktivierungsfehler-2026-08-24.md)
|
- [PROD activation incident analysis and 2.2.3 fix](docs/PROD-Installer-Aktivierungsfehler-2026-08-24.md)
|
||||||
- [Optional all-users desktop shortcut stability](docs/Installer-Optionale-Desktopverknuepfung-2026-08-24.md)
|
- [Optional all-users desktop shortcut stability](docs/Installer-Optionale-Desktopverknuepfung-2026-08-24.md)
|
||||||
- [ACC runtime shutdown incident and recovery fix](docs/ACC-Runtime-Shutdown-Exception-2026-08-19.md)
|
- [ACC runtime shutdown incident and recovery fix](docs/ACC-Runtime-Shutdown-Exception-2026-08-19.md)
|
||||||
|
- [PROD ScheduledTask control and runtime logging runbook](docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md)
|
||||||
- [AI maintainer handoff](AI-README.md)
|
- [AI maintainer handoff](AI-README.md)
|
||||||
- [References](REFERENCES.md)
|
- [References](REFERENCES.md)
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
- MSBTS_ReceiveLocation (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi
|
- MSBTS_ReceiveLocation (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi
|
||||||
- MSBTS_ReceiveLocation.Enable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-enable-method-wmi
|
- MSBTS_ReceiveLocation.Enable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-enable-method-wmi
|
||||||
- MSBTS_ReceiveLocation.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-wmi
|
- MSBTS_ReceiveLocation.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-wmi
|
||||||
|
- Microsoft.BizTalk.Scheduler.ISchedule: https://learn.microsoft.com/en-us/dotnet/api/microsoft.biztalk.scheduler.ischedule?view=bts-2020
|
||||||
|
- Managed BizTalk adapter assembly deployment and resolution: https://learn.microsoft.com/en-us/biztalk/core/how-biztalk-server-instantiates-an-adapter
|
||||||
|
- BizTalk Scheduled Task Adapter 7.0.2 release repository: https://github.com/sandroasp/BizTalk-Scheduled-Task-Adapter
|
||||||
- MSBTS_HostInstance (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-wmi
|
- MSBTS_HostInstance (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-wmi
|
||||||
- MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi
|
- MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi
|
||||||
- MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi
|
- MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# PROD-Runbook: ScheduledTask-Steuerung und persistentes Laufzeitlogging
|
||||||
|
|
||||||
|
**Stand:** 2026-08-26
|
||||||
|
|
||||||
|
**Zielversion:** 2.3.0
|
||||||
|
|
||||||
|
**Betroffener Adapter:** BizTalk ScheduledTask Adapter 7.0.2
|
||||||
|
|
||||||
|
**Bestätigter Adapterpfad:** `C:\Program Files (x86)\BizTalk ScheduledTask Adapter 7.0.2`
|
||||||
|
|
||||||
|
## Ursache und Fix
|
||||||
|
|
||||||
|
`MSBTS_ReceiveLocation.Enable` und `.Disable` validieren die Transportdaten einer ScheduledTask-Receive-Location. Der Adapter benötigt dafür `Microsoft.BizTalk.Scheduler.dll`. Diese BizTalk-Assembly liegt im BizTalk-Installationsverzeichnis, ist aber nicht in jeder Umgebung im GAC auflösbar. Der Fehler lautete deshalb sinngemäß `Could not load file or assembly Microsoft.BizTalk.Scheduler, Version=3.13.0.0`.
|
||||||
|
|
||||||
|
Version 2.3.0 erkennt ScheduledTask-Schritte an Adaptername oder `scheduler:`-URI. Unmittelbar vor der WMI-Mutation sucht sie die Assembly in der lokalen BizTalk-/Adapterinstallation, prüft ihre .NET-Assemblyidentität und lädt sie nur für den laufenden Toolprozess. Weitere angeforderte Abhängigkeiten werden nach derselben Identitätsprüfung aufgelöst. Das Tool kopiert keine Fremd-DLL, registriert nichts und verändert den GAC nicht.
|
||||||
|
|
||||||
|
## Vorbereitung
|
||||||
|
|
||||||
|
1. Vorhandene `before.json`, Ergebnisdateien und Laufzeitlogs außerhalb des Installationsordners sichern.
|
||||||
|
2. Setup 2.3.0 als Administrator installieren beziehungsweise aktualisieren.
|
||||||
|
3. Im Setup-Log `setup_version=2.3.0.0` und erfolgreichen Ziel-Self-Test prüfen.
|
||||||
|
4. Tool als Administrator starten und mit **Log Folder** `%ProgramData%\BizTalkPlatformManagementTool\Logs` öffnen.
|
||||||
|
5. Prüfen, dass der lokale BizTalk-Installationsordner `Microsoft.BizTalk.Scheduler.dll` enthält.
|
||||||
|
6. Nur wenn BizTalk oder der Adapter abweichend installiert wurde: `AdapterAssemblySearchPaths` in `BizTalkPlatformManagementTool.exe.config` um den vorhandenen lokalen Ordner ergänzen. Mehrere Pfade werden mit Semikolon getrennt. Keine DLL aus ACC, einer alten BizTalk-Version oder einem Downloadordner kopieren.
|
||||||
|
|
||||||
|
## Kontrollierter Funktionstest
|
||||||
|
|
||||||
|
1. **Diagnose** ausführen.
|
||||||
|
2. **Snapshot Before** ausführen und im Statusgrid die betroffene Receive Location mit Adapter und `scheduler:`-Adresse prüfen.
|
||||||
|
3. **Dry run** aktiviert lassen und **Shutdown** ausführen.
|
||||||
|
4. In `shutdown-plan.json` bei der betroffenen Receive Location `AdapterName` und `Address` prüfen.
|
||||||
|
5. Erst nach Planreview Dry-run deaktivieren, **Shutdown** erneut ausführen und bestätigen.
|
||||||
|
6. Im Operation Log müssen für den Scheduler-Schritt mindestens folgende Inhalte erscheinen:
|
||||||
|
- `ScheduledTask adapter preflight`
|
||||||
|
- `ProcessBitness=` und `SearchDirectories=`
|
||||||
|
- `ScheduledTask dependency loaded process-locally`, `resolved through CLR/GAC` oder `already loaded`
|
||||||
|
- `Calling MSBTS_ReceiveLocation.Disable`
|
||||||
|
- `Reached: Disable receive location completed`
|
||||||
|
7. In `shutdown-result.json` muss der Schritt `Succeeded` oder bei bereits erreichtem Zustand `AlreadySatisfied` sein.
|
||||||
|
8. Nach der Wartung **Restore** zunächst im Dry-run, danach real ausführen.
|
||||||
|
9. Entsprechend `Calling MSBTS_ReceiveLocation.Enable`, den erreichten Zustand und `Succeeded`/`AlreadySatisfied` prüfen.
|
||||||
|
10. **Snapshot After** und **Compare** ausführen; die Receive Location muss dem gespeicherten Sollzustand entsprechen.
|
||||||
|
|
||||||
|
## Logging-Test über Neustart
|
||||||
|
|
||||||
|
1. Einen erkennbaren Diagnose- oder Dry-run-Lauf durchführen und Uhrzeit notieren.
|
||||||
|
2. Tool regulär schließen und erneut starten.
|
||||||
|
3. Das Operation Log muss die Einträge des vorherigen Prozesses mit vollständigem Datum und Uhrzeit wieder anzeigen.
|
||||||
|
4. **Clear** leert nur das Grid. Nach erneutem Programmstart wird die aufbewahrte Historie wieder geladen.
|
||||||
|
5. **Log Folder** muss den lokalen Ordner öffnen.
|
||||||
|
6. Die aktuelle Tagesdatei heißt `BizTalkPlatformManagementTool-yyyy-MM-dd.log`.
|
||||||
|
7. Nach dem ersten Start an einem Folgetag wird der abgeschlossene Vortag zu `.log.gz` komprimiert.
|
||||||
|
8. Aktueller Tag plus 29 Vortage bleiben erhalten. Einträge außerhalb dieses Fensters werden beim Start entfernt.
|
||||||
|
9. Das Grid lädt höchstens die neuesten 10.000 Einträge; die 30-Tage-Dateien bleiben davon unabhängig vollständig erhalten.
|
||||||
|
|
||||||
|
## Fehlerfall und Supportpaket
|
||||||
|
|
||||||
|
Bei einem Fehler keine Assembly austauschen und den GAC nicht spontan verändern. Folgende unveränderte Evidenz sichern:
|
||||||
|
|
||||||
|
1. Aktuelle `.log` sowie relevante `.log.gz` aus dem über **Log Folder** geöffneten Ordner.
|
||||||
|
2. `shutdown-plan.json` oder `restore-plan.json`.
|
||||||
|
3. `shutdown-result.json`, `restore-result.json` oder den timestamp-basierten Emergency-Report.
|
||||||
|
4. `before.json` und vorhandenen Nachher-Snapshot.
|
||||||
|
5. `BizTalkPlatformManagementTool.exe.config`.
|
||||||
|
6. Screenshot des vollständigen roten Grid-Eintrags und der Umgebung.
|
||||||
|
7. Dateieigenschaften/Version von `Microsoft.BizTalk.Scheduler.dll`; die Datei selbst nur nach interner Freigabe übermitteln.
|
||||||
|
|
||||||
|
Der Fehlerdatensatz enthält Exceptiontyp, HRESULT, innere Ausnahmen, vorhandenes Fusion-Loaderprotokoll, Stacktrace, angeforderte Assembly, anfordernde Assembly und alle geprüften Suchpfade. Damit lässt sich unterscheiden zwischen fehlendem Pfad, falscher BizTalk-Version, nicht passender Strong-Name-Identität, Bitness-/Abhängigkeitsproblem und eigentlichem WMI-Fehler.
|
||||||
|
|
||||||
|
## Abnahmekriterien
|
||||||
|
|
||||||
|
- ScheduledTask-Receive-Location lässt sich real deaktivieren und wieder auf den Snapshotzustand aktivieren.
|
||||||
|
- Andere Receive Locations bleiben unverändert funktionsfähig und benötigen keinen Scheduler-Preflight.
|
||||||
|
- Ein einzelner Fehler bleibt `Failed`, verhindert aber keine späteren unabhängigen Planschritte.
|
||||||
|
- Ergebnisreport und Nachher-Snapshot werden auch bei einem Teilfehler soweit möglich geschrieben.
|
||||||
|
- Historisches Log erscheint nach Neustart wieder im Grid.
|
||||||
|
- Vortagslogs werden komprimiert und exakt 30 Kalendertage aufbewahrt.
|
||||||
|
- Setup, Tool und Ergebnisdateien melden Version 2.3.0 beziehungsweise `2.3.0-net461`.
|
||||||
|
|
||||||
|
Die lokale Mono-Toolchain prüft Resolverlogik, Identitätsgrenze, Persistenz, Kompression und Aufbewahrung ohne BizTalk. Die endgültige Freigabe erfordert diesen realen Windows-/BizTalk-/PROD-Test.
|
||||||
@@ -23,7 +23,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
|||||||
private const string ProductName = "BizTalk Platform Management Tool";
|
private const string ProductName = "BizTalk Platform Management Tool";
|
||||||
|
|
||||||
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
|
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
|
||||||
private const string ProductVersion = "2.2.4";
|
private const string ProductVersion = "2.3.0";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
|
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
|||||||
{
|
{
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
||||||
Text = "BizTalk Platform Management Tool 2.2.4"
|
Text = "BizTalk Platform Management Tool 2.3.0"
|
||||||
});
|
});
|
||||||
root.Controls.Add(new Label
|
root.Controls.Add(new Label
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||||
[assembly: ComVisible(false)]
|
[assembly: ComVisible(false)]
|
||||||
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
|
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
|
||||||
[assembly: AssemblyVersion("2.2.4.0")]
|
[assembly: AssemblyVersion("2.3.0.0")]
|
||||||
[assembly: AssemblyFileVersion("2.2.4.0")]
|
[assembly: AssemblyFileVersion("2.3.0.0")]
|
||||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<assemblyIdentity version="2.2.4.0" name="BizTalkPlatformManagementTool.Setup" />
|
<assemblyIdentity version="2.3.0.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
|
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
|
||||||
</trustInfo>
|
</trustInfo>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
|
<appSettings>
|
||||||
|
<!-- Semicolon-delimited optional override. Normal BizTalk 2020 and ScheduledTask 7.x folders are auto-discovered. -->
|
||||||
|
<add key="AdapterAssemblySearchPaths" value="%ProgramFiles(x86)%\Microsoft BizTalk Server 2020;%ProgramFiles(x86)%\BizTalk ScheduledTask Adapter 7.0.2" />
|
||||||
|
</appSettings>
|
||||||
<startup>
|
<startup>
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
|
||||||
</startup>
|
</startup>
|
||||||
|
|||||||
@@ -39,10 +39,12 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Configuration" />
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
<Reference Include="System.Drawing" />
|
<Reference Include="System.Drawing" />
|
||||||
<Reference Include="System.Management" />
|
<Reference Include="System.Management" />
|
||||||
<Reference Include="System.Runtime.Serialization" />
|
<Reference Include="System.Runtime.Serialization" />
|
||||||
|
<Reference Include="System.IO.Compression" />
|
||||||
<Reference Include="System.ServiceProcess" />
|
<Reference Include="System.ServiceProcess" />
|
||||||
<Reference Include="System.Windows.Forms" />
|
<Reference Include="System.Windows.Forms" />
|
||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
@@ -56,10 +58,12 @@
|
|||||||
<Compile Include="Models\DiffModels.cs" />
|
<Compile Include="Models\DiffModels.cs" />
|
||||||
<Compile Include="Models\OperationModels.cs" />
|
<Compile Include="Models\OperationModels.cs" />
|
||||||
<Compile Include="Services\BizTalkWmiClient.cs" />
|
<Compile Include="Services\BizTalkWmiClient.cs" />
|
||||||
|
<Compile Include="Services\AdapterAssemblyResolver.cs" />
|
||||||
<Compile Include="Services\CsvWriter.cs" />
|
<Compile Include="Services\CsvWriter.cs" />
|
||||||
<Compile Include="Services\HtmlReportWriter.cs" />
|
<Compile Include="Services\HtmlReportWriter.cs" />
|
||||||
<Compile Include="Services\JsonFileStore.cs" />
|
<Compile Include="Services\JsonFileStore.cs" />
|
||||||
<Compile Include="Services\OperationLogger.cs" />
|
<Compile Include="Services\OperationLogger.cs" />
|
||||||
|
<Compile Include="Services\ExceptionDiagnostics.cs" />
|
||||||
<Compile Include="Services\OperationPlanExecutor.cs" />
|
<Compile Include="Services\OperationPlanExecutor.cs" />
|
||||||
<Compile Include="Services\SnapshotComparer.cs" />
|
<Compile Include="Services\SnapshotComparer.cs" />
|
||||||
<Compile Include="Services\SnapshotValidator.cs" />
|
<Compile Include="Services\SnapshotValidator.cs" />
|
||||||
|
|||||||
@@ -184,6 +184,14 @@ namespace BizTalkPlatformManagementTool.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DataMember(Order = 13)]
|
[DataMember(Order = 13)]
|
||||||
public string Warning { get; set; }
|
public string Warning { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the receive adapter name used for dependency preflight.</summary>
|
||||||
|
[DataMember(Order = 14, EmitDefaultValue = false)]
|
||||||
|
public string AdapterName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the receive transport address used to identify scheduler URIs.</summary>
|
||||||
|
[DataMember(Order = 15, EmitDefaultValue = false)]
|
||||||
|
public string Address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||||
[assembly: ComVisible(false)]
|
[assembly: ComVisible(false)]
|
||||||
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
||||||
[assembly: AssemblyVersion("2.2.4.0")]
|
[assembly: AssemblyVersion("2.3.0.0")]
|
||||||
[assembly: AssemblyFileVersion("2.2.4.0")]
|
[assembly: AssemblyFileVersion("2.3.0.0")]
|
||||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Configuration;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves custom-adapter and BizTalk support assemblies from installed product
|
||||||
|
/// directories for the lifetime of one real operation-plan execution.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class AdapterAssemblyResolver : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>The dependency whose absence breaks ScheduledTask Enable/Disable validation.</summary>
|
||||||
|
internal const string SchedulerAssemblyName = "Microsoft.BizTalk.Scheduler";
|
||||||
|
|
||||||
|
/// <summary>Logger receiving all search-path and load diagnostics.</summary>
|
||||||
|
private readonly OperationLogger _logger;
|
||||||
|
|
||||||
|
/// <summary>Trusted directories searched after normal CLR resolution has failed.</summary>
|
||||||
|
private readonly List<string> _searchDirectories;
|
||||||
|
|
||||||
|
/// <summary>Prevents duplicate event registration and unregisters safely.</summary>
|
||||||
|
private bool _attached;
|
||||||
|
|
||||||
|
/// <summary>Prevents repeated ScheduledTask dependency preflight in one plan.</summary>
|
||||||
|
private bool _schedulerPrepared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes and attaches the process-local resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The operation logger used for support diagnostics.</param>
|
||||||
|
public AdapterAssemblyResolver(OperationLogger logger)
|
||||||
|
: this(logger, DiscoverSearchDirectories())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Initializes a resolver with explicit directories for regression tests.</summary>
|
||||||
|
/// <param name="logger">Optional logger.</param>
|
||||||
|
/// <param name="searchDirectories">Directories that may contain dependencies.</param>
|
||||||
|
internal AdapterAssemblyResolver(OperationLogger logger, IEnumerable<string> searchDirectories)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_searchDirectories = NormalizeDirectories(searchDirectories).ToList();
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
|
||||||
|
_attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the immutable ordered dependency search path.</summary>
|
||||||
|
internal IList<string> SearchDirectories
|
||||||
|
{
|
||||||
|
get { return _searchDirectories.AsReadOnly(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preloads the BizTalk Scheduler dependency before WMI validates a Scheduler
|
||||||
|
/// receive location and emits actionable diagnostics when it cannot be found.
|
||||||
|
/// </summary>
|
||||||
|
public void PrepareScheduledTaskAdapter()
|
||||||
|
{
|
||||||
|
if (_schedulerPrepared)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_schedulerPrepared = true;
|
||||||
|
|
||||||
|
Info("ScheduledTask adapter preflight. ProcessBitness=" + (Environment.Is64BitProcess ? "64" : "32")
|
||||||
|
+ "; SearchDirectories=" + string.Join(" | ", _searchDirectories.ToArray()));
|
||||||
|
|
||||||
|
var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.FirstOrDefault(x => string.Equals(x.GetName().Name, SchedulerAssemblyName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (alreadyLoaded != null)
|
||||||
|
{
|
||||||
|
Success("ScheduledTask dependency already loaded: " + DescribeAssembly(alreadyLoaded));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Normal CLR/GAC resolution remains authoritative. If it fails, the attached
|
||||||
|
// resolver gets the same request and may satisfy it from an installed product path.
|
||||||
|
var normallyResolved = Assembly.Load(new AssemblyName(SchedulerAssemblyName));
|
||||||
|
Success("ScheduledTask dependency resolved through CLR/GAC: " + DescribeAssembly(normallyResolved));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
// The explicit path diagnostic below reports every searched installation directory.
|
||||||
|
}
|
||||||
|
catch (FileLoadException ex)
|
||||||
|
{
|
||||||
|
Warning("ScheduledTask dependency CLR/GAC resolution found an incompatible or unloadable assembly. " + ExceptionDiagnostics.Format(ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = FindCandidateFile(SchedulerAssemblyName, null, _searchDirectories);
|
||||||
|
if (path == null)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
"ScheduledTask receive-location control requires Microsoft.BizTalk.Scheduler.dll. "
|
||||||
|
+ "The assembly was not found in the BizTalk installation directory, the ScheduledTask Adapter directory, "
|
||||||
|
+ "or configured AdapterAssemblySearchPaths. Search directories: "
|
||||||
|
+ string.Join(" | ", _searchDirectories.ToArray())
|
||||||
|
+ ". Install the matching BizTalk Scheduler assembly in the GAC or add its existing directory to "
|
||||||
|
+ "BizTalkPlatformManagementTool.exe.config; do not copy an assembly from another BizTalk version.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = Assembly.LoadFrom(path);
|
||||||
|
Success("ScheduledTask dependency loaded process-locally: " + DescribeAssembly(assembly));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"ScheduledTask dependency was found but could not be loaded from '" + path + "'. "
|
||||||
|
+ ExceptionDiagnostics.Format(ex), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Unregisters the process-local resolver.</summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_attached)
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly;
|
||||||
|
_attached = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Handles unresolved CLR assembly requests using identity-checked files.</summary>
|
||||||
|
private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requested = new AssemblyName(args.Name);
|
||||||
|
var loaded = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => AssemblyIdentityMatches(requested, x.GetName()));
|
||||||
|
if (loaded != null)
|
||||||
|
{
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = FindCandidateFile(requested.Name, requested, _searchDirectories);
|
||||||
|
if (path == null)
|
||||||
|
{
|
||||||
|
Warning("Assembly resolution failed. Requested=" + args.Name
|
||||||
|
+ "; RequestingAssembly=" + (args.RequestingAssembly == null ? "(unknown)" : args.RequestingAssembly.FullName)
|
||||||
|
+ "; SearchDirectories=" + string.Join(" | ", _searchDirectories.ToArray()));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var assembly = Assembly.LoadFrom(path);
|
||||||
|
Success("Resolved adapter dependency process-locally. Requested=" + args.Name
|
||||||
|
+ "; Loaded=" + DescribeAssembly(assembly));
|
||||||
|
return assembly;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Warning("Assembly resolution raised an error. Requested=" + args.Name + "; " + ExceptionDiagnostics.Format(ex));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an identity-compatible DLL without loading it into the current AppDomain.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="simpleName">Requested simple assembly name.</param>
|
||||||
|
/// <param name="requested">Full requested identity, or null to accept any installed version.</param>
|
||||||
|
/// <param name="directories">Trusted directories to search.</param>
|
||||||
|
/// <returns>The matching DLL path, or null.</returns>
|
||||||
|
internal static string FindCandidateFile(string simpleName, AssemblyName requested, IEnumerable<string> directories)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(simpleName))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
foreach (var directory in NormalizeDirectories(directories))
|
||||||
|
{
|
||||||
|
var candidate = Path.Combine(directory, simpleName + ".dll");
|
||||||
|
if (!File.Exists(candidate))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var actual = AssemblyName.GetAssemblyName(candidate);
|
||||||
|
if (requested == null
|
||||||
|
? string.Equals(actual.Name, simpleName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
: AssemblyIdentityMatches(requested, actual))
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// A native, corrupt or unrelated file is never loaded merely because its file name matches.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Compares simple name, requested version, culture and public key token.</summary>
|
||||||
|
private static bool AssemblyIdentityMatches(AssemblyName requested, AssemblyName actual)
|
||||||
|
{
|
||||||
|
if (requested == null || actual == null
|
||||||
|
|| !string.Equals(requested.Name, actual.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (requested.Version != null && actual.Version != requested.Version)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(requested.CultureName)
|
||||||
|
&& !string.Equals(requested.CultureName, actual.CultureName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var expectedToken = requested.GetPublicKeyToken();
|
||||||
|
var actualToken = actual.GetPublicKeyToken();
|
||||||
|
return expectedToken == null || expectedToken.Length == 0 || TokensEqual(expectedToken, actualToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Compares strong-name public key tokens.</summary>
|
||||||
|
private static bool TokensEqual(byte[] left, byte[] right)
|
||||||
|
{
|
||||||
|
if (left == null || right == null || left.Length != right.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < left.Length; i++)
|
||||||
|
{
|
||||||
|
if (left[i] != right[i])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Discovers BizTalk, ScheduledTask and explicitly configured directories.</summary>
|
||||||
|
private static IEnumerable<string> DiscoverSearchDirectories()
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
AddConfiguredDirectories(result);
|
||||||
|
AddRegistryDirectories(result);
|
||||||
|
|
||||||
|
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||||||
|
AddMatchingDirectories(result, programFilesX86, "Microsoft BizTalk Server*");
|
||||||
|
AddMatchingDirectories(result, programFilesX86, "BizTalk ScheduledTask Adapter*");
|
||||||
|
AddMatchingDirectories(result, programFilesX86, "Biztalk ScheduledTask Adapter*");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Adds semicolon-delimited paths from App.config after environment expansion.</summary>
|
||||||
|
private static void AddConfiguredDirectories(ICollection<string> result)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var configured = ConfigurationManager.AppSettings["AdapterAssemblySearchPaths"];
|
||||||
|
if (string.IsNullOrWhiteSpace(configured))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (var value in configured.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
result.Add(Environment.ExpandEnvironmentVariables(value.Trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Registry and conventional installation directories remain available.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Adds known assembly-path values from both registry views.</summary>
|
||||||
|
private static void AddRegistryDirectories(ICollection<string> result)
|
||||||
|
{
|
||||||
|
var views = new[] { RegistryView.Registry32, RegistryView.Registry64 };
|
||||||
|
foreach (var view in views)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
|
||||||
|
{
|
||||||
|
AddRegistryTreeDirectories(baseKey, @"SOFTWARE\Microsoft\BizTalk Server\3.0", result, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Registry discovery is best-effort and never blocks normal BizTalk adapters.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Reads installation and assembly directories from a shallow BizTalk registry tree.</summary>
|
||||||
|
private static void AddRegistryTreeDirectories(RegistryKey baseKey, string subKeyName, ICollection<string> result, int depth)
|
||||||
|
{
|
||||||
|
if (depth > 3)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using (var key = baseKey.OpenSubKey(subKeyName, false))
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (var valueName in key.GetValueNames())
|
||||||
|
{
|
||||||
|
var value = key.GetValue(valueName) as string;
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
value = Environment.ExpandEnvironmentVariables(value.Trim().Trim('"'));
|
||||||
|
if (value.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.Add(Path.GetDirectoryName(value));
|
||||||
|
}
|
||||||
|
else if (Directory.Exists(value))
|
||||||
|
{
|
||||||
|
result.Add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var child in key.GetSubKeyNames())
|
||||||
|
{
|
||||||
|
AddRegistryTreeDirectories(baseKey, subKeyName + "\\" + child, result, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Adds directories matching one non-recursive product-folder pattern.</summary>
|
||||||
|
private static void AddMatchingDirectories(ICollection<string> result, string parent, string pattern)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(parent))
|
||||||
|
{
|
||||||
|
foreach (var directory in Directory.GetDirectories(parent, pattern, SearchOption.TopDirectoryOnly))
|
||||||
|
{
|
||||||
|
result.Add(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// A denied optional discovery path is reported later if Scheduler preflight is required.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Normalizes, de-duplicates and filters existing directories.</summary>
|
||||||
|
private static IEnumerable<string> NormalizeDirectories(IEnumerable<string> directories)
|
||||||
|
{
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var value in directories ?? Enumerable.Empty<string>())
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
string fullPath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fullPath = Path.GetFullPath(value.Trim().Trim('"')).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Directory.Exists(fullPath) && seen.Add(fullPath))
|
||||||
|
{
|
||||||
|
yield return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Builds a diagnostic assembly description.</summary>
|
||||||
|
private static string DescribeAssembly(Assembly assembly)
|
||||||
|
{
|
||||||
|
return assembly.FullName + "; Location=" + assembly.Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Writes informational diagnostics when a logger is available.</summary>
|
||||||
|
private void Info(string message) { if (_logger != null) _logger.Info(message); }
|
||||||
|
|
||||||
|
/// <summary>Writes warning diagnostics when a logger is available.</summary>
|
||||||
|
private void Warning(string message) { if (_logger != null) _logger.Warning(message); }
|
||||||
|
|
||||||
|
/// <summary>Writes successful dependency diagnostics when a logger is available.</summary>
|
||||||
|
private void Success(string message) { if (_logger != null) _logger.Success(message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current tool version written into generated snapshots.
|
/// Current tool version written into generated snapshots.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Version = "2.2.4-net461";
|
public const string Version = "2.3.0-net461";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fallback application name used when WMI does not expose an application property.
|
/// Fallback application name used when WMI does not expose an application property.
|
||||||
@@ -192,7 +192,9 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
|
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
|
||||||
{
|
{
|
||||||
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null));
|
var step = Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null);
|
||||||
|
SetReceiveTransport(step, item);
|
||||||
|
plan.Steps.Add(step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +308,9 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
foreach (var item in app.ReceiveLocations)
|
foreach (var item in app.ReceiveLocations)
|
||||||
{
|
{
|
||||||
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null));
|
var step = Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null);
|
||||||
|
SetReceiveTransport(step, item);
|
||||||
|
plan.Steps.Add(step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +469,9 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// <summary>Operation logger.</summary>
|
/// <summary>Operation logger.</summary>
|
||||||
private readonly OperationLogger _logger;
|
private readonly OperationLogger _logger;
|
||||||
|
|
||||||
|
/// <summary>Process-local resolver for adapter validation dependencies.</summary>
|
||||||
|
private readonly AdapterAssemblyResolver _assemblyResolver;
|
||||||
|
|
||||||
/// <summary>Lazily connected BizTalk WMI client.</summary>
|
/// <summary>Lazily connected BizTalk WMI client.</summary>
|
||||||
private BizTalkWmiClient _client;
|
private BizTalkWmiClient _client;
|
||||||
|
|
||||||
@@ -475,6 +482,7 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_assemblyResolver = new AdapterAssemblyResolver(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Executes one Windows-service or BizTalk-WMI step state-aware.</summary>
|
/// <summary>Executes one Windows-service or BizTalk-WMI step state-aware.</summary>
|
||||||
@@ -499,6 +507,10 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
return RuntimeStepOutcome.AlreadySatisfied;
|
return RuntimeStepOutcome.AlreadySatisfied;
|
||||||
}
|
}
|
||||||
|
if (IsScheduledTaskReceiveLocation(step))
|
||||||
|
{
|
||||||
|
_assemblyResolver.PrepareScheduledTaskAdapter();
|
||||||
|
}
|
||||||
ExecuteWmiStep(instance, step, options);
|
ExecuteWmiStep(instance, step, options);
|
||||||
return RuntimeStepOutcome.Succeeded;
|
return RuntimeStepOutcome.Succeeded;
|
||||||
}
|
}
|
||||||
@@ -514,6 +526,20 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
_client.Dispose();
|
_client.Dispose();
|
||||||
_client = null;
|
_client = null;
|
||||||
}
|
}
|
||||||
|
_assemblyResolver.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Detects the third-party ScheduledTask adapter from captured plan metadata.</summary>
|
||||||
|
/// <param name="step">The receive-location step.</param>
|
||||||
|
/// <returns>True when Scheduler dependency preflight is required.</returns>
|
||||||
|
private static bool IsScheduledTaskReceiveLocation(OperationStep step)
|
||||||
|
{
|
||||||
|
return step != null
|
||||||
|
&& string.Equals(step.Kind, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& ((!string.IsNullOrWhiteSpace(step.Address)
|
||||||
|
&& step.Address.TrimStart().StartsWith("scheduler:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (!string.IsNullOrWhiteSpace(step.AdapterName)
|
||||||
|
&& step.AdapterName.IndexOf("schedul", StringComparison.OrdinalIgnoreCase) >= 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -735,6 +761,15 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Copies receive transport metadata into a durable operation step.</summary>
|
||||||
|
/// <param name="step">The plan step to enrich.</param>
|
||||||
|
/// <param name="receiveLocation">The captured receive location.</param>
|
||||||
|
private static void SetReceiveTransport(OperationStep step, ReceiveLocationState receiveLocation)
|
||||||
|
{
|
||||||
|
step.AdapterName = receiveLocation == null ? null : receiveLocation.AdapterName;
|
||||||
|
step.Address = receiveLocation == null ? null : receiveLocation.Address;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts integer method arguments into the object array required by WMI.
|
/// Converts integer method arguments into the object array required by WMI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -773,6 +808,8 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
+ ", class=" + step.WmiClass
|
+ ", class=" + step.WmiClass
|
||||||
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
||||||
+ ", method=" + step.MethodName
|
+ ", method=" + step.MethodName
|
||||||
|
+ (string.IsNullOrWhiteSpace(step.AdapterName) ? string.Empty : ", adapter=" + step.AdapterName)
|
||||||
|
+ (string.IsNullOrWhiteSpace(step.Address) ? string.Empty : ", address=" + step.Address)
|
||||||
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
|
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
|
||||||
+ "]";
|
+ "]";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates support-ready, single-line exception diagnostics for the GUI, durable
|
||||||
|
/// execution reports and rolling log files.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ExceptionDiagnostics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Formats the complete exception chain including type, HRESULT, fusion details
|
||||||
|
/// and stack trace without embedding physical line breaks in the daily log.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">The exception to format.</param>
|
||||||
|
/// <returns>A detailed single-line diagnostic string.</returns>
|
||||||
|
public static string Format(Exception exception)
|
||||||
|
{
|
||||||
|
if (exception == null)
|
||||||
|
{
|
||||||
|
return "Unknown operation error.";
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = new List<string>();
|
||||||
|
var current = exception;
|
||||||
|
var depth = 0;
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
var label = depth == 0 ? "Exception" : "InnerException[" + depth.ToString(CultureInfo.InvariantCulture) + "]";
|
||||||
|
var part = label
|
||||||
|
+ " Type=" + current.GetType().FullName
|
||||||
|
+ "; HResult=0x" + current.HResult.ToString("X8", CultureInfo.InvariantCulture)
|
||||||
|
+ "; Message=" + Flatten(current.Message);
|
||||||
|
|
||||||
|
var fileNotFound = current as FileNotFoundException;
|
||||||
|
if (fileNotFound != null && !string.IsNullOrWhiteSpace(fileNotFound.FusionLog))
|
||||||
|
{
|
||||||
|
part += "; FusionLog=" + Flatten(fileNotFound.FusionLog);
|
||||||
|
}
|
||||||
|
var fileLoad = current as FileLoadException;
|
||||||
|
if (fileLoad != null && !string.IsNullOrWhiteSpace(fileLoad.FusionLog))
|
||||||
|
{
|
||||||
|
part += "; FusionLog=" + Flatten(fileLoad.FusionLog);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(current.StackTrace))
|
||||||
|
{
|
||||||
|
part += "; StackTrace=" + Flatten(current.StackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.Add(part);
|
||||||
|
current = current.InnerException;
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
return string.Join(" | ", parts.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Replaces physical control characters with readable escape sequences.</summary>
|
||||||
|
/// <param name="value">The text to flatten.</param>
|
||||||
|
/// <returns>Single-line text suitable for one durable log record.</returns>
|
||||||
|
private static string Flatten(string value)
|
||||||
|
{
|
||||||
|
return (value ?? string.Empty)
|
||||||
|
.Replace("\r", "\\r")
|
||||||
|
.Replace("\n", "\\n")
|
||||||
|
.Replace("\t", "\\t");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
@@ -68,20 +71,18 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
private const string LogFileExtension = ".log";
|
private const string LogFileExtension = ".log";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of daily log files retained, including the current day.
|
/// Number of calendar days retained, including the current day.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int RetentionDays = 5;
|
internal const int RetentionDays = 30;
|
||||||
|
|
||||||
|
/// <summary>Maximum number of historical records restored into the GUI by default.</summary>
|
||||||
|
public const int DefaultGridHistoryLimit = 10000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process-wide lock that serializes log file appends.
|
/// Process-wide lock that serializes log file appends.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly object FileLock = new object();
|
private static readonly object FileLock = new object();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process-wide flag that ensures log cleanup runs only once.
|
|
||||||
/// </summary>
|
|
||||||
private static int _cleanupDone;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional callback for forwarding entries to the UI.
|
/// Optional callback for forwarding entries to the UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -97,12 +98,24 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sink">Optional callback that receives entries for display.</param>
|
/// <param name="sink">Optional callback that receives entries for display.</param>
|
||||||
public OperationLogger(Action<LogEntry> sink)
|
public OperationLogger(Action<LogEntry> sink)
|
||||||
|
: this(sink, ResolveLogDirectory())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Initializes a logger with an explicit directory for regression tests.</summary>
|
||||||
|
/// <param name="sink">Optional callback that receives new entries.</param>
|
||||||
|
/// <param name="logDirectory">Directory used for plain and compressed logs.</param>
|
||||||
|
internal OperationLogger(Action<LogEntry> sink, string logDirectory)
|
||||||
{
|
{
|
||||||
_sink = sink;
|
_sink = sink;
|
||||||
_logDirectory = ResolveLogDirectory();
|
_logDirectory = Path.GetFullPath(logDirectory);
|
||||||
CleanupOldLogs();
|
Directory.CreateDirectory(_logDirectory);
|
||||||
|
MaintainLogs(DateTime.Now.Date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the directory containing active and compressed runtime logs.</summary>
|
||||||
|
public string LogDirectory { get { return _logDirectory; } }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the path of the daily log file for the current date.
|
/// Gets the path of the daily log file for the current date.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -114,6 +127,42 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads retained plain and GZip-compressed records for restoring the operation grid.
|
||||||
|
/// Malformed legacy lines are ignored without affecting current logging.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maximumEntries">Maximum newest entries to return.</param>
|
||||||
|
/// <returns>Chronologically ordered retained log entries.</returns>
|
||||||
|
public IList<LogEntry> ReadRecentEntries(int maximumEntries)
|
||||||
|
{
|
||||||
|
if (maximumEntries <= 0)
|
||||||
|
{
|
||||||
|
return new List<LogEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = new List<LogEntry>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var files = Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension + "*")
|
||||||
|
.Where(IsSupportedLogFile)
|
||||||
|
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
ReadEntries(file, entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Historical display is optional; current operations and file logging continue.
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.OrderBy(x => x.Timestamp)
|
||||||
|
.Skip(Math.Max(0, entries.Count - maximumEntries))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes an informational entry.
|
/// Writes an informational entry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -202,7 +251,7 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
"[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
|
"[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
|
||||||
entry.Timestamp,
|
entry.Timestamp,
|
||||||
entry.Level.ToString().ToUpperInvariant(),
|
entry.Level.ToString().ToUpperInvariant(),
|
||||||
entry.Message,
|
(entry.Message ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n"),
|
||||||
Environment.NewLine);
|
Environment.NewLine);
|
||||||
|
|
||||||
lock (FileLock)
|
lock (FileLock)
|
||||||
@@ -217,24 +266,29 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes log files older than the configured retention window.
|
/// Compresses completed daily logs and removes all records outside retention.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void CleanupOldLogs()
|
/// <param name="today">The current local date.</param>
|
||||||
|
internal void MaintainLogs(DateTime today)
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _cleanupDone, 1) == 1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cutoff = DateTime.Now.Date.AddDays(-(RetentionDays - 1));
|
var cutoff = today.Date.AddDays(-(RetentionDays - 1));
|
||||||
foreach (var file in Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension))
|
foreach (var file in Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension + "*"))
|
||||||
{
|
{
|
||||||
var lastWrite = File.GetLastWriteTime(file);
|
DateTime fileDate;
|
||||||
if (lastWrite.Date < cutoff)
|
if (!TryGetLogDate(file, out fileDate))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fileDate < cutoff)
|
||||||
{
|
{
|
||||||
File.Delete(file);
|
File.Delete(file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fileDate < today.Date && file.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Compress(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,6 +298,117 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Compresses a completed log atomically and removes the plain source afterward.</summary>
|
||||||
|
/// <param name="source">The completed plain log.</param>
|
||||||
|
private static void Compress(string source)
|
||||||
|
{
|
||||||
|
var target = source + ".gz";
|
||||||
|
if (File.Exists(target))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var temporary = target + ".tmp." + Guid.NewGuid().ToString("N");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var input = File.OpenRead(source))
|
||||||
|
using (var output = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||||
|
using (var gzip = new GZipStream(output, CompressionLevel.Optimal))
|
||||||
|
{
|
||||||
|
input.CopyTo(gzip);
|
||||||
|
}
|
||||||
|
File.Move(temporary, target);
|
||||||
|
File.Delete(source);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(temporary))
|
||||||
|
{
|
||||||
|
File.Delete(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Reads one supported plain or compressed file into an entry collection.</summary>
|
||||||
|
private static void ReadEntries(string path, ICollection<LogEntry> entries)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||||
|
using (var payload = path.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? (Stream)new GZipStream(file, CompressionMode.Decompress)
|
||||||
|
: file)
|
||||||
|
using (var reader = new StreamReader(payload, Encoding.UTF8, true))
|
||||||
|
{
|
||||||
|
string line;
|
||||||
|
while ((line = reader.ReadLine()) != null)
|
||||||
|
{
|
||||||
|
LogEntry entry;
|
||||||
|
if (TryParse(line, out entry))
|
||||||
|
{
|
||||||
|
entries.Add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// One unreadable archive must not hide all other retained history.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses the stable daily log line format.</summary>
|
||||||
|
/// <param name="line">One physical line.</param>
|
||||||
|
/// <param name="entry">Parsed entry when successful.</param>
|
||||||
|
/// <returns>True for a valid record.</returns>
|
||||||
|
internal static bool TryParse(string line, out LogEntry entry)
|
||||||
|
{
|
||||||
|
entry = null;
|
||||||
|
if (string.IsNullOrEmpty(line) || line.Length < 29 || line[0] != '[' || line[21] != '[')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var timestampEnd = line.IndexOf(']');
|
||||||
|
var levelEnd = line.IndexOf(']', timestampEnd + 1);
|
||||||
|
DateTime timestamp;
|
||||||
|
LogLevel level;
|
||||||
|
if (timestampEnd != 20 || levelEnd < 0
|
||||||
|
|| !DateTime.TryParseExact(line.Substring(1, 19), "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out timestamp)
|
||||||
|
|| !Enum.TryParse(line.Substring(timestampEnd + 2, levelEnd - timestampEnd - 2), true, out level))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var messageStart = levelEnd + 1;
|
||||||
|
if (messageStart < line.Length && line[messageStart] == ' ')
|
||||||
|
{
|
||||||
|
messageStart++;
|
||||||
|
}
|
||||||
|
entry = new LogEntry { Timestamp = timestamp, Level = level, Message = line.Substring(messageStart) };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Checks the supported exact file suffixes.</summary>
|
||||||
|
private static bool IsSupportedLogFile(string path)
|
||||||
|
{
|
||||||
|
return path.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| path.EndsWith(LogFileExtension + ".gz", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Extracts the calendar date from a stable log file name.</summary>
|
||||||
|
private static bool TryGetLogDate(string path, out DateTime date)
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(path);
|
||||||
|
var suffixLength = name.EndsWith(LogFileExtension + ".gz", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? (LogFileExtension + ".gz").Length
|
||||||
|
: name.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase) ? LogFileExtension.Length : 0;
|
||||||
|
if (suffixLength == 0 || !name.StartsWith(LogFilePrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
date = default(DateTime);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var value = name.Substring(LogFilePrefix.Length, name.Length - LogFilePrefix.Length - suffixLength);
|
||||||
|
return DateTime.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ermittelt das bevorzugte maschinenweite Logverzeichnis mit Rückfall auf das EXE-Verzeichnis.
|
/// Ermittelt das bevorzugte maschinenweite Logverzeichnis mit Rückfall auf das EXE-Verzeichnis.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -179,6 +179,8 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
+ ", class=" + step.WmiClass
|
+ ", class=" + step.WmiClass
|
||||||
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
||||||
+ ", method=" + step.MethodName
|
+ ", method=" + step.MethodName
|
||||||
|
+ (string.IsNullOrWhiteSpace(step.AdapterName) ? string.Empty : ", adapter=" + step.AdapterName)
|
||||||
|
+ (string.IsNullOrWhiteSpace(step.Address) ? string.Empty : ", address=" + step.Address)
|
||||||
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
|
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
|
||||||
+ "]";
|
+ "]";
|
||||||
}
|
}
|
||||||
@@ -188,21 +190,7 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// <returns>A single diagnostic message.</returns>
|
/// <returns>A single diagnostic message.</returns>
|
||||||
internal static string FormatException(Exception exception)
|
internal static string FormatException(Exception exception)
|
||||||
{
|
{
|
||||||
if (exception == null)
|
return ExceptionDiagnostics.Format(exception);
|
||||||
{
|
|
||||||
return "Unknown operation error.";
|
|
||||||
}
|
|
||||||
var message = exception.Message;
|
|
||||||
var inner = exception.InnerException;
|
|
||||||
while (inner != null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0)
|
|
||||||
{
|
|
||||||
message += " Inner error: " + inner.Message;
|
|
||||||
}
|
|
||||||
inner = inner.InnerException;
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Writes an informational message when a logger is available.</summary>
|
/// <summary>Writes an informational message when a logger is available.</summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -125,6 +126,9 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private Button _clearButton;
|
private Button _clearButton;
|
||||||
|
|
||||||
|
/// <summary>Button that opens the persistent runtime log directory.</summary>
|
||||||
|
private Button _openLogsButton;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Button that closes the application.
|
/// Button that closes the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -150,6 +154,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_service = new BizTalkOperationService(_logger);
|
_service = new BizTalkOperationService(_logger);
|
||||||
BuildUi();
|
BuildUi();
|
||||||
FormClosing += MainFormClosing;
|
FormClosing += MainFormClosing;
|
||||||
|
LoadLogHistory();
|
||||||
_logger.Info("Log file: " + _logger.LogFilePath);
|
_logger.Info("Log file: " + _logger.LogFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +277,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_emergencyRestoreButton = ActionButton("Emergency Restore", EmergencyRestoreClick);
|
_emergencyRestoreButton = ActionButton("Emergency Restore", EmergencyRestoreClick);
|
||||||
_emergencyRestoreButton.Width = 142;
|
_emergencyRestoreButton.Width = 142;
|
||||||
_clearButton = ActionButton("Clear", ClearClick);
|
_clearButton = ActionButton("Clear", ClearClick);
|
||||||
|
_openLogsButton = ActionButton("Log Folder", OpenLogsClick);
|
||||||
_closeButton = ActionButton("Close", CloseClick);
|
_closeButton = ActionButton("Close", CloseClick);
|
||||||
|
|
||||||
panel.Controls.Add(_diagnoseButton);
|
panel.Controls.Add(_diagnoseButton);
|
||||||
@@ -283,6 +289,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
panel.Controls.Add(_validateStateButton);
|
panel.Controls.Add(_validateStateButton);
|
||||||
panel.Controls.Add(_emergencyRestoreButton);
|
panel.Controls.Add(_emergencyRestoreButton);
|
||||||
panel.Controls.Add(_clearButton);
|
panel.Controls.Add(_clearButton);
|
||||||
|
panel.Controls.Add(_openLogsButton);
|
||||||
panel.Controls.Add(_closeButton);
|
panel.Controls.Add(_closeButton);
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
@@ -306,7 +313,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
statusPage.Controls.Add(_statusGrid);
|
statusPage.Controls.Add(_statusGrid);
|
||||||
|
|
||||||
ConfigureGrid(_logGrid);
|
ConfigureGrid(_logGrid);
|
||||||
_logGrid.Columns.Add("Time", "Time");
|
_logGrid.Columns.Add("Timestamp", "Timestamp");
|
||||||
_logGrid.Columns.Add("Level", "Level");
|
_logGrid.Columns.Add("Level", "Level");
|
||||||
_logGrid.Columns.Add("Message", "Message");
|
_logGrid.Columns.Add("Message", "Message");
|
||||||
_logGrid.Columns[2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
_logGrid.Columns[2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
||||||
@@ -577,6 +584,22 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_statusLabel.Text = "Ready.";
|
_statusLabel.Text = "Ready.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Opens the persistent local runtime log directory in Windows Explorer.</summary>
|
||||||
|
/// <param name="sender">The control that raised the event.</param>
|
||||||
|
/// <param name="e">The event arguments.</param>
|
||||||
|
private void OpenLogsClick(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_logger.LogDirectory);
|
||||||
|
Process.Start("explorer.exe", _logger.LogDirectory);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error("Could not open runtime log directory. " + ExceptionDiagnostics.Format(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the Close button click.
|
/// Handles the Close button click.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -816,24 +839,53 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
{
|
{
|
||||||
InvokeIfRequired(() =>
|
InvokeIfRequired(() =>
|
||||||
{
|
{
|
||||||
var index = _logGrid.Rows.Add(entry.Timestamp.ToString("HH:mm:ss"), entry.Level.ToString(), entry.Message);
|
AddLogRow(entry);
|
||||||
var row = _logGrid.Rows[index];
|
|
||||||
if (entry.Level == LogLevel.Error)
|
|
||||||
{
|
|
||||||
row.DefaultCellStyle.ForeColor = Color.DarkRed;
|
|
||||||
}
|
|
||||||
else if (entry.Level == LogLevel.Warning)
|
|
||||||
{
|
|
||||||
row.DefaultCellStyle.ForeColor = Color.DarkOrange;
|
|
||||||
}
|
|
||||||
else if (entry.Level == LogLevel.Success)
|
|
||||||
{
|
|
||||||
row.DefaultCellStyle.ForeColor = Color.DarkGreen;
|
|
||||||
}
|
|
||||||
_logGrid.FirstDisplayedScrollingRowIndex = Math.Max(0, _logGrid.Rows.Count - 1);
|
_logGrid.FirstDisplayedScrollingRowIndex = Math.Max(0, _logGrid.Rows.Count - 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Restores retained plain and compressed log records into the grid at startup.</summary>
|
||||||
|
private void LoadLogHistory()
|
||||||
|
{
|
||||||
|
var entries = _logger.ReadRecentEntries(OperationLogger.DefaultGridHistoryLimit);
|
||||||
|
_logGrid.SuspendLayout();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
AddLogRow(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_logGrid.ResumeLayout();
|
||||||
|
}
|
||||||
|
if (_logGrid.Rows.Count > 0)
|
||||||
|
{
|
||||||
|
_logGrid.FirstDisplayedScrollingRowIndex = _logGrid.Rows.Count - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Adds and colors one retained or live log row.</summary>
|
||||||
|
/// <param name="entry">The log entry to display.</param>
|
||||||
|
private void AddLogRow(LogEntry entry)
|
||||||
|
{
|
||||||
|
var index = _logGrid.Rows.Add(entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"), entry.Level.ToString(), entry.Message);
|
||||||
|
var row = _logGrid.Rows[index];
|
||||||
|
if (entry.Level == LogLevel.Error)
|
||||||
|
{
|
||||||
|
row.DefaultCellStyle.ForeColor = Color.DarkRed;
|
||||||
|
}
|
||||||
|
else if (entry.Level == LogLevel.Warning)
|
||||||
|
{
|
||||||
|
row.DefaultCellStyle.ForeColor = Color.DarkOrange;
|
||||||
|
}
|
||||||
|
else if (entry.Level == LogLevel.Success)
|
||||||
|
{
|
||||||
|
row.DefaultCellStyle.ForeColor = Color.DarkGreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enables or disables action buttons and updates the status strip.
|
/// Enables or disables action buttons and updates the status strip.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -853,6 +905,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_validateStateButton.Enabled = !busy;
|
_validateStateButton.Enabled = !busy;
|
||||||
_emergencyRestoreButton.Enabled = !busy;
|
_emergencyRestoreButton.Enabled = !busy;
|
||||||
_clearButton.Enabled = !busy;
|
_clearButton.Enabled = !busy;
|
||||||
|
_openLogsButton.Enabled = !busy;
|
||||||
_closeButton.Enabled = !busy;
|
_closeButton.Enabled = !busy;
|
||||||
_statusLabel.Text = status;
|
_statusLabel.Text = status;
|
||||||
});
|
});
|
||||||
@@ -1071,19 +1124,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
return "Operation failed.";
|
return "Operation failed.";
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = ex.Message;
|
return ExceptionDiagnostics.Format(ex);
|
||||||
var inner = ex.InnerException;
|
|
||||||
while (inner != null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0)
|
|
||||||
{
|
|
||||||
message += " Inner error: " + inner.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
inner = inner.InnerException;
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<assemblyIdentity version="2.2.4.0" name="BizTalkPlatformManagementTool" />
|
<assemblyIdentity version="2.3.0.0" name="BizTalkPlatformManagementTool" />
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
<security>
|
<security>
|
||||||
<requestedPrivileges>
|
<requestedPrivileges>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using BizTalkPlatformManagementTool.Models;
|
using BizTalkPlatformManagementTool.Models;
|
||||||
using BizTalkPlatformManagementTool.Services;
|
using BizTalkPlatformManagementTool.Services;
|
||||||
@@ -35,6 +36,10 @@ namespace BizTalkPlatformManagementTool.Tests
|
|||||||
Run("LoggerSinkFailureCannotAbortPlanExecution", LoggerSinkFailureCannotAbortPlanExecution);
|
Run("LoggerSinkFailureCannotAbortPlanExecution", LoggerSinkFailureCannotAbortPlanExecution);
|
||||||
Run("PlanExecutionSkipsAlreadySatisfiedState", PlanExecutionSkipsAlreadySatisfiedState);
|
Run("PlanExecutionSkipsAlreadySatisfiedState", PlanExecutionSkipsAlreadySatisfiedState);
|
||||||
Run("ExecutionReportRoundTripPreservesFailure", ExecutionReportRoundTripPreservesFailure);
|
Run("ExecutionReportRoundTripPreservesFailure", ExecutionReportRoundTripPreservesFailure);
|
||||||
|
Run("ScheduledReceivePlanCarriesAdapterMetadata", ScheduledReceivePlanCarriesAdapterMetadata);
|
||||||
|
Run("AdapterAssemblyResolverRequiresMatchingIdentity", AdapterAssemblyResolverRequiresMatchingIdentity);
|
||||||
|
Run("OperationLogPersistsCompressesAndRetainsThirtyDays", OperationLogPersistsCompressesAndRetainsThirtyDays);
|
||||||
|
Run("ExceptionDiagnosticsContainSupportContext", ExceptionDiagnosticsContainSupportContext);
|
||||||
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
|
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
|
||||||
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
|
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
|
||||||
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
|
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
|
||||||
@@ -169,6 +174,96 @@ namespace BizTalkPlatformManagementTool.Tests
|
|||||||
Assert(lastSendPort < firstHost, "host instances were not globally last");
|
Assert(lastSendPort < firstHost, "host instances were not globally last");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft, dass Scheduler-Erkennung auch nach Planpersistenz möglich bleibt.</summary>
|
||||||
|
private static void ScheduledReceivePlanCarriesAdapterMetadata()
|
||||||
|
{
|
||||||
|
var snapshot = Snapshot("APP", "SP", ArtifactStates.SendPortStarted);
|
||||||
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState
|
||||||
|
{
|
||||||
|
Application = "APP",
|
||||||
|
Name = "RV_PMP_Trigger_Schedule",
|
||||||
|
Enabled = true,
|
||||||
|
AdapterName = "Schedule",
|
||||||
|
Address = "scheduler://PMP-trigger"
|
||||||
|
});
|
||||||
|
var service = new BizTalkOperationService(null);
|
||||||
|
var shutdown = service.CreateShutdownPlan(snapshot, snapshot.Server);
|
||||||
|
var step = shutdown.Steps.Single(x => x.Kind == "ReceiveLocation");
|
||||||
|
Assert(step.AdapterName == "Schedule", "adapter name missing from shutdown plan");
|
||||||
|
Assert(step.Address == "scheduler://PMP-trigger", "scheduler URI missing from shutdown plan");
|
||||||
|
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var path = Path.Combine(directory, "plan.json");
|
||||||
|
JsonFileStore.Save(path, shutdown);
|
||||||
|
var loaded = JsonFileStore.Load<OperationPlan>(path);
|
||||||
|
Assert(loaded.Steps.Single(x => x.Kind == "ReceiveLocation").Address.StartsWith("scheduler:", StringComparison.OrdinalIgnoreCase), "scheduler metadata did not survive serialization");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft Dateinamen- und vollständige Assemblyidentität vor prozesslokalem Laden.</summary>
|
||||||
|
private static void AdapterAssemblyResolverRequiresMatchingIdentity()
|
||||||
|
{
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var source = typeof(BizTalkOperationService).Assembly.Location;
|
||||||
|
var identity = AssemblyName.GetAssemblyName(source);
|
||||||
|
var candidate = Path.Combine(directory, identity.Name + ".dll");
|
||||||
|
File.Copy(source, candidate);
|
||||||
|
Assert(AdapterAssemblyResolver.FindCandidateFile(identity.Name, identity, new[] { directory }) == candidate, "matching assembly identity was not found");
|
||||||
|
|
||||||
|
var wrongVersion = new AssemblyName(identity.FullName);
|
||||||
|
wrongVersion.Version = new Version(identity.Version.Major + 1, 0, 0, 0);
|
||||||
|
Assert(AdapterAssemblyResolver.FindCandidateFile(identity.Name, wrongVersion, new[] { directory }) == null, "wrong assembly version was accepted");
|
||||||
|
|
||||||
|
File.Copy(source, Path.Combine(directory, AdapterAssemblyResolver.SchedulerAssemblyName + ".dll"));
|
||||||
|
Assert(AdapterAssemblyResolver.FindCandidateFile(AdapterAssemblyResolver.SchedulerAssemblyName, null, new[] { directory }) == null, "file-name-only scheduler impostor was accepted");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft Neustart-Rehydration, Vortagskompression und die 30-Tage-Grenze.</summary>
|
||||||
|
private static void OperationLogPersistsCompressesAndRetainsThirtyDays()
|
||||||
|
{
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var today = DateTime.Today;
|
||||||
|
var yesterday = Path.Combine(directory, "BizTalkPlatformManagementTool-" + today.AddDays(-1).ToString("yyyy-MM-dd") + ".log");
|
||||||
|
var expired = Path.Combine(directory, "BizTalkPlatformManagementTool-" + today.AddDays(-30).ToString("yyyy-MM-dd") + ".log");
|
||||||
|
File.WriteAllText(yesterday, "[" + today.AddDays(-1).ToString("yyyy-MM-dd") + " 08:00:00][WARNING] retained warning\r\n", Encoding.UTF8);
|
||||||
|
File.WriteAllText(expired, "[" + today.AddDays(-30).ToString("yyyy-MM-dd") + " 08:00:00][INFO] expired\r\n", Encoding.UTF8);
|
||||||
|
|
||||||
|
var logger = new OperationLogger(null, directory);
|
||||||
|
Assert(File.Exists(yesterday + ".gz") && !File.Exists(yesterday), "completed daily log was not compressed");
|
||||||
|
Assert(!File.Exists(expired), "log outside 30-day retention was not deleted");
|
||||||
|
logger.Error("durable failure marker");
|
||||||
|
|
||||||
|
var restarted = new OperationLogger(null, directory);
|
||||||
|
var entries = restarted.ReadRecentEntries(100);
|
||||||
|
Assert(entries.Any(x => x.Level == LogLevel.Warning && x.Message == "retained warning"), "compressed history was not restored");
|
||||||
|
Assert(entries.Any(x => x.Level == LogLevel.Error && x.Message == "durable failure marker"), "current history was not restored after restart");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft Typ, HRESULT, innere Ausnahme und Stacktrace für Supportfälle.</summary>
|
||||||
|
private static void ExceptionDiagnosticsContainSupportContext()
|
||||||
|
{
|
||||||
|
Exception failure;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("outer detail", new FileNotFoundException("scheduler dependency missing"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failure = ex;
|
||||||
|
}
|
||||||
|
var diagnostic = ExceptionDiagnostics.Format(failure);
|
||||||
|
Assert(diagnostic.Contains("Type=System.InvalidOperationException"), "exception type missing");
|
||||||
|
Assert(diagnostic.Contains("HResult=0x"), "HRESULT missing");
|
||||||
|
Assert(diagnostic.Contains("InnerException[1]"), "inner exception index missing");
|
||||||
|
Assert(diagnostic.Contains("scheduler dependency missing"), "inner message missing");
|
||||||
|
Assert(diagnostic.Contains("StackTrace="), "stack trace missing");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Prüft den vollständigen Shutdown-Fortgang von Receive Location bis Host Instance nach einem frühen Fehler.</summary>
|
/// <summary>Prüft den vollständigen Shutdown-Fortgang von Receive Location bis Host Instance nach einem frühen Fehler.</summary>
|
||||||
private static void ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure()
|
private static void ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user