diff --git a/.gitignore b/.gitignore
index 51b6516..d7da0c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@ Thumbs.db
.idea/
*.user
*.suo
+bin/
+obj/
# Logs / Outputs
*.log
diff --git a/BizTalkPlatformManagementTool.sln b/BizTalkPlatformManagementTool.sln
new file mode 100644
index 0000000..0f2dfdf
--- /dev/null
+++ b/BizTalkPlatformManagementTool.sln
@@ -0,0 +1,21 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 15
+VisualStudioVersion = 15.0.28307.168
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool", "src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj", "{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95b5c2e..1394fa5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
# Changelog
+## [Unreleased] - 2026-04-27
+### Changed
+- Projekt auf `BizTalkPlatformManagementTool` umbenannt.
+- Solution, Projektdatei, Namespace, Assemblyname und Standard-Ausgabepfad angepasst.
+- Dokumentation aktualisiert und `Installation.md` ergänzt.
+
## [1.3.0] - 2026-02-02
### Added
- HTML Report mit farblicher Kennzeichnung (grün/rot)
diff --git a/Dokumentation.md b/Dokumentation.md
new file mode 100644
index 0000000..3a51471
--- /dev/null
+++ b/Dokumentation.md
@@ -0,0 +1,89 @@
+# BizTalk Platform Management Tool Dokumentation
+
+**Stand:** 2026-04-27
+**Implementierung:** C# WinForms, .NET Framework 4.7.1
+**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
+
+## Zweck
+
+Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster für Microsoft BizTalk Server 2020:
+
+- Exakten Laufzeitstatus vor einer Wartung erfassen.
+- Receive Locations, Orchestrations, Send Ports und Host Instances in definierter Reihenfolge herunterfahren.
+- Den Laufzeitstatus aus einer gespeicherten `before.json` wiederherstellen.
+- Den Status nach der Wartung erfassen und beide Snapshots vergleichen.
+- Plan- und Ergebnisdateien für Review, Audit und Nachvollziehbarkeit schreiben.
+
+## Projektstruktur
+
+- Solution: `BizTalkPlatformManagementTool.sln`
+- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
+- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs`
+- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs`
+- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs`
+- PowerShell-Archiv: `archive/powershell/BizTalkPlatformManagementTool.ps1`
+
+## UI Workflow
+
+1. BizTalk-Servernamen eintragen.
+2. Ausgabeverzeichnis wählen.
+3. **Dry run** aktiviert lassen.
+4. Mit **Diagnose** den Zugriff auf `root\MicrosoftBizTalkServer` prüfen.
+5. Mit **Snapshot Before** die Datei `before.json` erzeugen.
+6. Mit **Shutdown** den Plan `shutdown-plan.json` erzeugen und prüfen.
+7. **Dry run** erst deaktivieren, wenn der Plan korrekt ist.
+8. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen.
+9. Mit **Snapshot After** und **Compare** die Umgebung validieren.
+
+## Sicherheitsdesign
+
+- Dry-run ist standardmäßig aktiviert.
+- Echte Shutdown-/Restore-Aktionen verlangen bei deaktiviertem Dry-run eine zusätzliche Bestätigung.
+- Jede Operation schreibt Einträge in das sichtbare Operation Log.
+- Operationspläne werden vor Laufzeitänderungen gespeichert.
+- WMI-Methodenrückgaben werden geprüft.
+- Wartezeiten nutzen konfigurierbare Timeout- und Polling-Werte.
+- Host Instances auf anderen Servern werden übersprungen und als Warnung protokolliert.
+
+## Shutdown-Reihenfolge
+
+1. Receive Locations deaktivieren, die im Snapshot aktiviert waren.
+2. Orchestrations stoppen, die gestartet waren.
+3. Send Ports stoppen, die gestartet waren.
+4. Host Instances stoppen, die auf dem ausgewählten Server gestartet waren.
+
+## Restore-Reihenfolge
+
+1. Host Instances starten, die auf dem ausgewählten Server zuvor gestartet waren.
+2. Send Ports wiederherstellen.
+3. Orchestrations wiederherstellen, soweit dies sicher möglich ist.
+4. Receive Locations zuletzt wiederherstellen.
+
+## Restore-Grenzen
+
+- Send Ports werden auf `Started`, `Stopped` oder `Bound` zurückgesetzt.
+- Orchestrations mit Status `Started`, `Stopped` oder `Unbound` werden direkt behandelt.
+- 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.
+
+## Ausgabedateien
+
+- Snapshots: `before.json`, `after.json`
+- Operationspläne: `shutdown-plan.json`, `restore-plan.json`
+- Nachher-Snapshots: `shutdown-after.json`, `restore-after.json`
+- Diff: `diff.json`, `diff.csv`, `diff.html`
+- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html`
+
+## Status Mapping
+
+- `MSBTS_SendPort.Status`: `1=Bound`, `2=Stopped`, `3=Started`
+- `MSBTS_Orchestration.OrchestrationStatus`: `1=Unbound`, `2=Bound`, `3=Stopped`, `4=Started`
+- `MSBTS_ReceiveLocation`: `Enabled = !IsDisabled`
+- `MSBTS_HostInstance.ServiceState`: `1=Stopped`, `2=StartPending`, `3=StopPending`, `4=Started`
+
+## Anforderungen
+
+- Windows Server oder Administrationshost mit .NET Framework 4.7.1
+- BizTalk Server 2020 oder installierte BizTalk Administration Tools
+- Berechtigungen auf den WMI-Namespace `root\MicrosoftBizTalkServer`
+- Visual Studio mit .NET Framework 4.7.1 Developer Pack für Builds
diff --git a/Installation.md b/Installation.md
new file mode 100644
index 0000000..2f7f088
--- /dev/null
+++ b/Installation.md
@@ -0,0 +1,37 @@
+# Installation
+
+## Voraussetzungen
+
+- Windows Server 2019/2022 oder ein Windows-Administrationshost
+- Microsoft BizTalk Server 2020 oder BizTalk Administration Tools
+- .NET Framework 4.7.1 Runtime
+- Für Builds: Visual Studio mit .NET Framework 4.7.1 Developer Pack
+- Zugriff auf den WMI-Namespace `root\MicrosoftBizTalkServer`
+- Ausreichende Rechte zum Lesen und Ändern von BizTalk-Artefakten
+
+## Build
+
+1. Repository öffnen.
+2. `BizTalkPlatformManagementTool.sln` in Visual Studio öffnen.
+3. Konfiguration `Release|Any CPU` auswählen.
+4. Solution bauen.
+5. Das Ergebnis liegt unter `src\BizTalkPlatformManagementTool\bin\Release\`.
+
+## Deployment
+
+1. Den Release-Ordner auf einen BizTalk-Server oder einen Administrationshost kopieren.
+2. Sicherstellen, dass der ausführende Benutzer WMI-Zugriff auf `root\MicrosoftBizTalkServer` hat.
+3. `BizTalkPlatformManagementTool.exe` starten.
+4. Als Ausgabeverzeichnis einen Ordner wählen, in dem Plan-, Snapshot- und Report-Dateien abgelegt werden dürfen.
+
+## Erster Funktionstest
+
+1. Anwendung starten.
+2. Zielserver eintragen oder den vorgeschlagenen lokalen Server verwenden.
+3. **Dry run** aktiviert lassen.
+4. **Diagnose** ausführen.
+5. **Snapshot Before** ausführen und prüfen, ob `before.json` sowie CSV/HTML-Reports erzeugt wurden.
+
+## Produktive Nutzung
+
+Vor produktiven Änderungen immer zuerst einen Dry-run ausführen und die erzeugten `shutdown-plan.json` beziehungsweise `restore-plan.json` prüfen. Dry-run erst deaktivieren, wenn der Plan fachlich und technisch korrekt ist.
diff --git a/README.md b/README.md
index 0718f0e..8bae6fb 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,70 @@
-# BizTalk Status Tool (PowerShell only)
+# BizTalk Platform Management Tool
-**Version:** 1.3.0
-**Kompatibel mit:** Windows Server 2019, PowerShell 5.1, BizTalk Server 2020
+WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations during maintenance windows. The tool captures runtime snapshots, creates shutdown and restore plans, executes those plans in a safe order, and writes diff reports for validation.
+
+## Current State
+
+- Application: C# WinForms
+- Target framework: .NET Framework 4.7.1
+- Solution: `BizTalkPlatformManagementTool.sln`
+- Project: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
+- Primary namespace and assembly: `BizTalkPlatformManagementTool`
+- Legacy PowerShell archive: `archive/powershell/BizTalkPlatformManagementTool.ps1`
## Features
-- Snapshots **vor**/**nach** Downtime als JSON
-- **Diff** zwischen Snapshots (nur Änderungen)
-- **HTML‑Report** mit **Farben** (grün = Started/Enabled, rot = Stopped/Disabled/Unbound/Bound)
-- **Remote‑fähig**: `-Server` Parameter (WMI unter `root\MicrosoftBizTalkServer`)
-- **Logging**: Konsole + `BizTalkStatusTool.log`
-## Verwendung (Beispiele)
-```powershell
-# Snapshot VOR Downtime vom Remote‑Server
-.\src\BizTalkStatusTool.ps1 -Before -Server "BTAPP01" -OutDir .\work
+- Snapshot before and after downtime as JSON, CSV and HTML
+- Diff between `before.json` and `after.json`
+- Controlled shutdown from the current runtime state
+- Controlled restore from `before.json`
+- Host instance handling for the selected BizTalk server
+- Dry-run mode enabled by default
+- WMI access through `root\MicrosoftBizTalkServer`
+- No compile-time dependency on BizTalk ExplorerOM assemblies
-# Snapshot NACH Downtime
-.\src\BizTalkStatusTool.ps1 -After -Server "BTAPP01" -OutDir .\work
+## Safe Usage
-# Diff erstellen (Text + HTML im gleichen Ordner)
-cd .\work
-..\src\BizTalkStatusTool.ps1 -Compare
-```
+1. Start the app on a BizTalk server or a management host with WMI access.
+2. Keep **Dry run** enabled.
+3. Click **Diagnose** to verify WMI access.
+4. Click **Snapshot Before**.
+5. Click **Shutdown** and review `shutdown-plan.json`.
+6. Disable **Dry run** only when the plan is correct.
+7. After maintenance, click **Restore** using the saved `before.json`.
+8. Click **Snapshot After** and **Compare**.
-## Status‑Mapping
-- **SendPort.Status**: 1=Bound, 2=Stopped, 3=Started
-- **Orchestration.OrchestrationStatus**: 1=Unbound, 2=Bound, 3=Stopped, 4=Started
-- **ReceiveLocation**: `Enabled = -not IsDisabled`
+## Operation Order
-**Hinweis:** Diese Mappings stammen aus der offiziellen BizTalk‑Dokumentation (siehe `REFERENCES.md`).
+Shutdown:
+- Disable receive locations that were enabled.
+- Stop orchestrations that were started.
+- Stop send ports that were started.
+- Stop host instances that were started on the selected server.
-## Releases
-Fertige Skripte liegen im Ordner `/release`.
+Restore:
+- Start host instances that were previously started.
+- Restore send ports to Started, Stopped or Bound.
+- Restore orchestrations where safe.
+- Restore receive locations last.
-## Tags
-Mit `tools/Create-Tags.ps1` kannst du lokale Git‑Tags (`v1.0.0`, `v1.1.0`, `v1.2.0`, `v1.3.0`) erstellen.
+Orchestrations that were `Bound` are deliberately left unchanged during restore to avoid accidentally making them `Unbound`.
+
+## Outputs
+
+- `before.json`, `after.json`
+- `shutdown-plan.json`, `restore-plan.json`
+- `shutdown-after.json`, `restore-after.json`
+- `diff.json`, `diff.csv`, `diff.html`
+- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
+
+## Build
+
+Open `BizTalkPlatformManagementTool.sln` in Visual Studio on Windows with the .NET Framework 4.7.1 Developer Pack installed, then build the `Release|Any CPU` configuration.
+
+The app targets .NET Framework 4.7.1 because BizTalk Server 2020 commonly runs in that Windows/.NET Framework operational environment.
+
+## Documentation
+
+- [Installation](Installation.md)
+- [Dokumentation](Dokumentation.md)
+- [References](REFERENCES.md)
diff --git a/REFERENCES.md b/REFERENCES.md
index 0fe4950..c52a97f 100644
--- a/REFERENCES.md
+++ b/REFERENCES.md
@@ -3,5 +3,17 @@
- MSBTS_SendPort.Status (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-status-property-wmi
- MSBTS_SendPort (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-wmi
+- MSBTS_SendPort.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-start-method-wmi
+- MSBTS_SendPort.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-stop-method-wmi
+- MSBTS_SendPort.Enlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-enlist-method-wmi
+- MSBTS_SendPort.UnEnlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-unenlist-method-wmi
- MSBTS_Orchestration.OrchestrationStatus (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-orchestrationstatus-property-wmi
+- MSBTS_Orchestration.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-start-method-wmi
+- MSBTS_Orchestration.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-stop-method-wmi
+- MSBTS_Orchestration.Unenlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-unenlist-method-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.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-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.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi
diff --git a/archive/powershell/BizTalkPlatformManagementTool.ps1 b/archive/powershell/BizTalkPlatformManagementTool.ps1
new file mode 100644
index 0000000..7303bd3
--- /dev/null
+++ b/archive/powershell/BizTalkPlatformManagementTool.ps1
@@ -0,0 +1,1040 @@
+
+<#
+BizTalk Platform Management Tool – Refactored Level B (Single File)
+Resilienz-Level 1 • HTML-Level B • PS 5.1 & 7.x kompatibel
+
+Beinhaltet:
+- Snapshot: Applications (ReceiveLocations / SendPorts / Orchestrations) + Host Instances
+- Exporte: JSON, CSV, HTML (modernisiert)
+- Compare: Before/After (Artifacts + HostInstances) → JSON, CSV, HTML, Console
+- Farbiges Console-Logging (wie Original)
+- Stabilere WMI-Zugriffe (Fallback auf CIM für PS 7.x)
+- Fix: HostInstance-Status (int ↔ text) robust
+#>
+
+# ============================= PARAMETERS =============================
+param(
+ [switch]$Before,
+ [switch]$After,
+ [switch]$Compare,
+ [switch]$Diag,
+ [switch]$Shutdown,
+ [switch]$Restore,
+ [switch]$PlanOnly,
+ [string]$Server,
+ [string]$MgmtDbServer,
+ [string]$MgmtDbName,
+ [string]$OutDir = ".",
+ [string]$StateFile = "before.json",
+ [int]$WaitTimeoutSec = 300,
+ [int]$PollIntervalSec = 5
+)
+
+if (-not $Server) { $Server = $env:COMPUTERNAME }
+$ErrorActionPreference = "Stop"
+$Global:LogFile = Join-Path (Get-Location) "BizTalkPlatformManagementTool.log"
+
+# ============================= LOGGING =============================
+function Write-Log {
+ param([string]$Message,[ValidateSet('INFO','WARN','ERROR','DEBUG')]$Level='INFO')
+ $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
+ $line = "[$ts][$Level] $Message"
+ $fg = switch ($Level) { INFO{'Gray'} WARN{'Yellow'} ERROR{'Red'} DEBUG{'DarkCyan'} }
+ Write-Host $line -ForegroundColor $fg
+ Add-Content -Path $Global:LogFile -Value $line
+}
+
+# ============================= UTILITIES =============================
+function Test-CommandExists {
+ param([Parameter(Mandatory)][string]$Name)
+ try { return [bool](Get-Command -Name $Name -ErrorAction Stop) } catch { return $false }
+}
+
+function Invoke-BizTalkWmiQuery {
+ <#
+ Cross-PS-Version Wrapper.
+ - In PS 5.1 nutzt Get-WmiObject
+ - In PS 7.x (und falls WMI nicht verfügbar) nutzt Get-CimInstance
+ #>
+ param(
+ [Parameter(Mandatory)][string]$ClassName,
+ [Parameter()][string]$Namespace = 'root\MicrosoftBizTalkServer',
+ [Parameter()][string]$ComputerName = $env:COMPUTERNAME,
+ [switch]$ThrowOnError
+ )
+ $useWmi = Test-CommandExists -Name 'Get-WmiObject'
+ try {
+ if ($useWmi) {
+ return Get-WmiObject -Class $ClassName -Namespace $Namespace -ComputerName $ComputerName -ErrorAction Stop
+ } else {
+ return Get-CimInstance -ClassName $ClassName -Namespace $Namespace -ComputerName $ComputerName -ErrorAction Stop
+ }
+ }
+ catch {
+ Write-Log "WMI/CIM query failed for ${ClassName}: $($_.Exception.Message)" 'ERROR'
+ if ($ThrowOnError) { throw }
+ return @()
+ }
+}
+
+# ============================= STATUS MAPPING =============================
+function Convert-HostState {
+ param($Value)
+ if ($Value -is [int]) { return $Value }
+ switch ($Value.ToString().ToLower()) {
+ 'started' { return 4 }
+ 'startpending' { return 2 }
+ 'stoppending' { return 3 }
+ 'stopped' { return 1 }
+ default { return 0 }
+ }
+}
+
+function Format-Status {
+ param([string]$ArtifactType, $Value)
+
+ $r = [ordered]@{Text='Unknown'; Class='bad'; ConsoleColor='Red'}
+
+ switch ($ArtifactType) {
+ 'ReceiveLocation' {
+ if ($Value -eq $true) { $r.Text='Enabled'; $r.Class='ok'; $r.ConsoleColor='Green' }
+ elseif ($Value -eq $false) { $r.Text='Disabled'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ }
+ 'SendPort' {
+ switch ([int]$Value) {
+ 3 { $r.Text='Started'; $r.Class='ok'; $r.ConsoleColor='Green' }
+ 2 { $r.Text='Stopped'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ 1 { $r.Text='Bound'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ default { $r.Text='Unknown'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ }
+ }
+ 'Orchestration' {
+ switch ([int]$Value) {
+ 4 { $r.Text='Started'; $r.Class='ok'; $r.ConsoleColor='Green' }
+ 3 { $r.Text='Stopped'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ 2 { $r.Text='Bound'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ 1 { $r.Text='Unbound'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ default { $r.Text='Unknown'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ }
+ }
+ 'HostInstance' {
+ $v = Convert-HostState $Value
+ switch ($v) {
+ 4 { $r.Text='Started'; $r.Class='ok'; $r.ConsoleColor='Green' }
+ 2 { $r.Text='StartPending'; $r.Class='warn'; $r.ConsoleColor='Yellow' }
+ 3 { $r.Text='StopPending'; $r.Class='warn'; $r.ConsoleColor='Yellow' }
+ 1 { $r.Text='Stopped'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ default { $r.Text='Unknown'; $r.Class='bad'; $r.ConsoleColor='Red' }
+ }
+ }
+ }
+ return [pscustomobject]$r
+}
+
+# ============================= MGMT DB =============================
+function Get-BizTalkMgmtDbInfo {
+ param($MgmtDbServer,$MgmtDbName)
+ try {
+ $p = Get-ItemProperty 'HKLM:SOFTWARE\\Microsoft\\BizTalk Server\\3.0\\Administration'
+ if (-not $MgmtDbServer) { $MgmtDbServer = $p.MgmtDBServer }
+ if (-not $MgmtDbName) { $MgmtDbName = $p.MgmtDBName }
+ } catch {
+ if (-not $MgmtDbServer) { $MgmtDbServer = 'localhost' }
+ if (-not $MgmtDbName) { $MgmtDbName = 'BizTalkMgmtDb' }
+ }
+ return @{ Server=$MgmtDbServer; Name=$MgmtDbName }
+}
+
+# ============================= HOST INSTANCES =============================
+function Get-BizTalkHostInstances {
+ param([string]$Server)
+
+ $list = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_HostInstance' -ComputerName $Server
+
+ $result = foreach ($hi in $list) {
+ $st = Format-Status -ArtifactType 'HostInstance' -Value $hi.ServiceState
+ [pscustomobject]@{
+ InstanceName = $hi.InstanceName
+ HostName = $hi.HostName
+ Server = $hi.RunningServer
+ RawState = $hi.ServiceState
+ StateText = $st.Text
+ StateClass = $st.Class
+ ConsoleColor = $st.ConsoleColor
+ }
+ }
+
+ return $result
+}
+
+# ============================= CONTROL HELPERS =============================
+function Get-BizTalkStateFile {
+ param([Parameter(Mandatory)][string]$Path)
+ if (-not (Test-Path $Path)) {
+ throw "State file not found: $Path"
+ }
+ return (Get-Content $Path -Raw | ConvertFrom-Json)
+}
+
+function Get-ReturnCode {
+ param($Result)
+ if ($null -eq $Result) { return $null }
+ if ($Result.PSObject.Properties.Name -contains 'ReturnValue') { return [int]$Result.ReturnValue }
+ if ($Result -is [int] -or $Result -is [uint32]) { return [int]$Result }
+ return $null
+}
+
+function Assert-BizTalkMethodResult {
+ param(
+ [Parameter(Mandatory)][string]$Action,
+ $Result
+ )
+
+ $code = Get-ReturnCode -Result $Result
+ if ($null -ne $code -and $code -ne 0) {
+ throw "$Action failed with HRESULT/ReturnValue $code"
+ }
+}
+
+function Invoke-BizTalkObjectMethod {
+ param(
+ [Parameter(Mandatory)]$InputObject,
+ [Parameter(Mandatory)][string]$MethodName,
+ [hashtable]$CimArguments = @{},
+ [object[]]$WmiArguments = @(),
+ [Parameter(Mandatory)][string]$Action,
+ [switch]$PlanOnly
+ )
+
+ if ($PlanOnly) {
+ Write-Log "PLAN: $Action"
+ return
+ }
+
+ Write-Log $Action
+
+ try {
+ if ($InputObject -is [Microsoft.Management.Infrastructure.CimInstance]) {
+ if ($CimArguments.Count -gt 0) {
+ $result = Invoke-CimMethod -InputObject $InputObject -MethodName $MethodName -Arguments $CimArguments -ErrorAction Stop
+ } else {
+ $result = Invoke-CimMethod -InputObject $InputObject -MethodName $MethodName -ErrorAction Stop
+ }
+ } else {
+ $args = if ($WmiArguments.Count -gt 0) { [object[]]$WmiArguments } else { $null }
+ $result = $InputObject.InvokeMethod($MethodName, $args)
+ }
+
+ Assert-BizTalkMethodResult -Action $Action -Result $result
+ Write-Log "OK: $Action"
+ }
+ catch {
+ Write-Log "FAILED: $Action - $($_.Exception.Message)" 'ERROR'
+ throw
+ }
+}
+
+function New-NameMap {
+ param(
+ [Parameter(Mandatory)][AllowEmptyCollection()]$Items,
+ [Parameter(Mandatory)][string]$KeyProperty
+ )
+
+ $map = @{}
+ foreach ($item in $Items) {
+ $key = $item.$KeyProperty
+ if (-not $key) { continue }
+ if ($map.ContainsKey($key)) {
+ Write-Log "Duplicate WMI key '$key' for property '$KeyProperty'; keeping first object." 'WARN'
+ continue
+ }
+ $map[$key] = $item
+ }
+ return $map
+}
+
+function Wait-BizTalkCondition {
+ param(
+ [Parameter(Mandatory)][scriptblock]$Condition,
+ [Parameter(Mandatory)][string]$Description,
+ [int]$TimeoutSec = 300,
+ [int]$PollIntervalSec = 5,
+ [switch]$PlanOnly
+ )
+
+ if ($PlanOnly) { return }
+
+ $deadline = (Get-Date).AddSeconds($TimeoutSec)
+ do {
+ if (& $Condition) {
+ Write-Log "Reached: $Description"
+ return
+ }
+ Start-Sleep -Seconds $PollIntervalSec
+ } while ((Get-Date) -lt $deadline)
+
+ throw "Timeout after ${TimeoutSec}s while waiting for: $Description"
+}
+
+function Get-CurrentBizTalkObjectMaps {
+ param([Parameter(Mandatory)][string]$Server)
+
+ $receiveLocations = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_ReceiveLocation' -ComputerName $Server -ThrowOnError
+ $sendPorts = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server -ThrowOnError
+ $orchestrations = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_Orchestration' -ComputerName $Server -ThrowOnError
+ $hostInstances = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_HostInstance' -ComputerName $Server -ThrowOnError
+
+ return [pscustomobject]@{
+ ReceiveLocations = New-NameMap -Items $receiveLocations -KeyProperty 'Name'
+ SendPorts = New-NameMap -Items $sendPorts -KeyProperty 'Name'
+ Orchestrations = New-NameMap -Items $orchestrations -KeyProperty 'Name'
+ HostInstances = New-NameMap -Items $hostInstances -KeyProperty 'InstanceName'
+ }
+}
+
+function Get-SnapshotArtifacts {
+ param([Parameter(Mandatory)]$Snapshot)
+
+ $receiveLocations = @()
+ $sendPorts = @()
+ $orchestrations = @()
+
+ foreach ($app in $Snapshot.Applications) {
+ foreach ($item in $app.ReceiveLocations) {
+ $receiveLocations += [pscustomobject]@{ Application=$app.Application; Name=$item.Name; Enabled=$item.Enabled }
+ }
+ foreach ($item in $app.SendPorts) {
+ $sendPorts += [pscustomobject]@{ Application=$app.Application; Name=$item.Name; Status=[int]$item.Status }
+ }
+ foreach ($item in $app.Orchestrations) {
+ $orchestrations += [pscustomobject]@{ Application=$app.Application; Name=$item.Name; Status=[int]$item.OrchestrationStatus }
+ }
+ }
+
+ return [pscustomobject]@{
+ ReceiveLocations = $receiveLocations
+ SendPorts = $sendPorts
+ Orchestrations = $orchestrations
+ HostInstances = @($Snapshot.HostInstances)
+ }
+}
+
+function Export-BizTalkOperationPlan {
+ param(
+ [Parameter(Mandatory)][string]$Path,
+ [Parameter(Mandatory)][string]$Mode,
+ [Parameter(Mandatory)]$Artifacts
+ )
+
+ $plan = [pscustomobject]@{
+ Mode = $Mode
+ CreatedAt = (Get-Date).ToString('s')
+ ReceiveLocations = @($Artifacts.ReceiveLocations)
+ SendPorts = @($Artifacts.SendPorts)
+ Orchestrations = @($Artifacts.Orchestrations)
+ HostInstances = @($Artifacts.HostInstances)
+ }
+
+ $plan | ConvertTo-Json -Depth 8 | Out-File $Path -Encoding UTF8
+ Write-Log "Operation plan exported: $Path"
+}
+
+function Stop-BizTalkRuntimeFromSnapshot {
+ param(
+ [Parameter(Mandatory)]$Snapshot,
+ [Parameter(Mandatory)][string]$Server,
+ [int]$WaitTimeoutSec,
+ [int]$PollIntervalSec,
+ [switch]$PlanOnly
+ )
+
+ $artifacts = Get-SnapshotArtifacts -Snapshot $Snapshot
+ Export-BizTalkOperationPlan -Path 'shutdown-plan.json' -Mode 'Shutdown' -Artifacts $artifacts
+
+ $maps = Get-CurrentBizTalkObjectMaps -Server $Server
+
+ foreach ($rl in $artifacts.ReceiveLocations | Where-Object { $_.Enabled -eq $true }) {
+ if (-not $maps.ReceiveLocations.ContainsKey($rl.Name)) { Write-Log "Receive location not found: $($rl.Name)" 'WARN'; continue }
+ $obj = $maps.ReceiveLocations[$rl.Name]
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Disable' -Action "Disable receive location '$($rl.Name)'" -PlanOnly:$PlanOnly
+ Wait-BizTalkCondition -Description "Receive location '$($rl.Name)' disabled" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition {
+ $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_ReceiveLocation' -ComputerName $Server | Where-Object { $_.Name -eq $rl.Name } | Select-Object -First 1
+ $current -and $current.IsDisabled -eq $true
+ }
+ }
+
+ foreach ($orch in $artifacts.Orchestrations | Where-Object { $_.Status -eq 4 }) {
+ if (-not $maps.Orchestrations.ContainsKey($orch.Name)) { Write-Log "Orchestration not found: $($orch.Name)" 'WARN'; continue }
+ $obj = $maps.Orchestrations[$orch.Name]
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -CimArguments @{ AutoDisableReceiveLocationFlag=[uint32]1; AutoSuspendServiceInstanceFlag=[uint32]1 } -WmiArguments @(1,1) -Action "Stop orchestration '$($orch.Name)'" -PlanOnly:$PlanOnly
+ Wait-BizTalkCondition -Description "Orchestration '$($orch.Name)' stopped" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition {
+ $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_Orchestration' -ComputerName $Server | Where-Object { $_.Name -eq $orch.Name } | Select-Object -First 1
+ $current -and [int]$current.OrchestrationStatus -eq 3
+ }
+ }
+
+ foreach ($sp in $artifacts.SendPorts | Where-Object { $_.Status -eq 3 }) {
+ if (-not $maps.SendPorts.ContainsKey($sp.Name)) { Write-Log "Send port not found: $($sp.Name)" 'WARN'; continue }
+ $obj = $maps.SendPorts[$sp.Name]
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -Action "Stop send port '$($sp.Name)'" -PlanOnly:$PlanOnly
+ Wait-BizTalkCondition -Description "Send port '$($sp.Name)' stopped" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition {
+ $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server | Where-Object { $_.Name -eq $sp.Name } | Select-Object -First 1
+ $current -and [int]$current.Status -eq 2
+ }
+ }
+
+ foreach ($hi in $artifacts.HostInstances | Where-Object { (Convert-HostState $_.RawState) -eq 4 }) {
+ if ($hi.Server -and $hi.Server -ne $Server) {
+ Write-Log "Skipping host instance '$($hi.InstanceName)' on server '$($hi.Server)'. Run the shutdown on that server to stop it safely." 'WARN'
+ continue
+ }
+ if (-not $maps.HostInstances.ContainsKey($hi.InstanceName)) { Write-Log "Host instance not found: $($hi.InstanceName)" 'WARN'; continue }
+ $obj = $maps.HostInstances[$hi.InstanceName]
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -Action "Stop host instance '$($hi.InstanceName)'" -PlanOnly:$PlanOnly
+ Wait-BizTalkCondition -Description "Host instance '$($hi.InstanceName)' stopped" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition {
+ $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_HostInstance' -ComputerName $Server | Where-Object { $_.InstanceName -eq $hi.InstanceName } | Select-Object -First 1
+ $current -and (Convert-HostState $current.ServiceState) -eq 1
+ }
+ }
+}
+
+function Restore-BizTalkRuntimeFromSnapshot {
+ param(
+ [Parameter(Mandatory)]$Snapshot,
+ [Parameter(Mandatory)][string]$Server,
+ [int]$WaitTimeoutSec,
+ [int]$PollIntervalSec,
+ [switch]$PlanOnly
+ )
+
+ $artifacts = Get-SnapshotArtifacts -Snapshot $Snapshot
+ Export-BizTalkOperationPlan -Path 'restore-plan.json' -Mode 'Restore' -Artifacts $artifacts
+
+ $maps = Get-CurrentBizTalkObjectMaps -Server $Server
+
+ foreach ($hi in $artifacts.HostInstances | Where-Object { (Convert-HostState $_.RawState) -eq 4 }) {
+ if ($hi.Server -and $hi.Server -ne $Server) {
+ Write-Log "Skipping host instance '$($hi.InstanceName)' on server '$($hi.Server)'. Run the restore on that server to start it safely." 'WARN'
+ continue
+ }
+ if (-not $maps.HostInstances.ContainsKey($hi.InstanceName)) { Write-Log "Host instance not found: $($hi.InstanceName)" 'WARN'; continue }
+ $obj = $maps.HostInstances[$hi.InstanceName]
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Start' -Action "Start host instance '$($hi.InstanceName)'" -PlanOnly:$PlanOnly
+ Wait-BizTalkCondition -Description "Host instance '$($hi.InstanceName)' started" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition {
+ $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_HostInstance' -ComputerName $Server | Where-Object { $_.InstanceName -eq $hi.InstanceName } | Select-Object -First 1
+ $current -and (Convert-HostState $current.ServiceState) -eq 4
+ }
+ }
+
+ foreach ($sp in $artifacts.SendPorts) {
+ if (-not $maps.SendPorts.ContainsKey($sp.Name)) { Write-Log "Send port not found: $($sp.Name)" 'WARN'; continue }
+ $obj = $maps.SendPorts[$sp.Name]
+ $currentStatus = [int]$obj.Status
+ switch ($sp.Status) {
+ 3 {
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Start' -Action "Start send port '$($sp.Name)'" -PlanOnly:$PlanOnly
+ }
+ 2 {
+ if ($currentStatus -eq 1) {
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Enlist' -Action "Enlist send port '$($sp.Name)' to stopped state" -PlanOnly:$PlanOnly
+ } elseif ($currentStatus -eq 3) {
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -Action "Stop send port '$($sp.Name)'" -PlanOnly:$PlanOnly
+ } else {
+ Write-Log "Send port already stopped: $($sp.Name)"
+ }
+ }
+ 1 {
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'UnEnlist' -Action "Unenlist send port '$($sp.Name)'" -PlanOnly:$PlanOnly
+ }
+ }
+ Wait-BizTalkCondition -Description "Send port '$($sp.Name)' restored to status $($sp.Status)" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition {
+ $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server | Where-Object { $_.Name -eq $sp.Name } | Select-Object -First 1
+ $current -and [int]$current.Status -eq $sp.Status
+ }
+ }
+
+ foreach ($orch in $artifacts.Orchestrations) {
+ if (-not $maps.Orchestrations.ContainsKey($orch.Name)) { Write-Log "Orchestration not found: $($orch.Name)" 'WARN'; continue }
+ $obj = $maps.Orchestrations[$orch.Name]
+ $currentStatus = [int]$obj.OrchestrationStatus
+ switch ($orch.Status) {
+ 4 {
+ if ($currentStatus -eq 4) {
+ Write-Log "Orchestration already started: $($orch.Name)"
+ } else {
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Start' -CimArguments @{ AutoEnableReceiveLocationFlag=[uint32]1; AutoResumeOrchestrationInstanceFlag=[uint32]1; AutoStartSendPortsFlag=[uint32]1 } -WmiArguments @(1,1,1) -Action "Start orchestration '$($orch.Name)'" -PlanOnly:$PlanOnly
+ }
+ }
+ 3 {
+ if ($currentStatus -eq 4) {
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -CimArguments @{ AutoDisableReceiveLocationFlag=[uint32]1; AutoSuspendServiceInstanceFlag=[uint32]1 } -WmiArguments @(1,1) -Action "Stop orchestration '$($orch.Name)'" -PlanOnly:$PlanOnly
+ } elseif ($currentStatus -eq 3) {
+ Write-Log "Orchestration already stopped: $($orch.Name)"
+ } else {
+ Write-Log "Orchestration '$($orch.Name)' target is Stopped, current status is $currentStatus. Leaving unchanged." 'WARN'
+ }
+ }
+ 2 {
+ Write-Log "Orchestration '$($orch.Name)' was Bound in the snapshot. Bound restore is left unchanged to avoid accidentally changing it to Unbound." 'WARN'
+ }
+ 1 {
+ if ($currentStatus -eq 1) {
+ Write-Log "Orchestration already unbound: $($orch.Name)"
+ } else {
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'UnenlistService' -CimArguments @{ AutoTerminateOrchestrationInstanceFlag=[uint32]1 } -WmiArguments @(1) -Action "Unenlist orchestration '$($orch.Name)'" -PlanOnly:$PlanOnly
+ }
+ }
+ }
+ if ($orch.Status -ne 2) {
+ Wait-BizTalkCondition -Description "Orchestration '$($orch.Name)' restored to status $($orch.Status)" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition {
+ $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_Orchestration' -ComputerName $Server | Where-Object { $_.Name -eq $orch.Name } | Select-Object -First 1
+ $current -and [int]$current.OrchestrationStatus -eq $orch.Status
+ }
+ }
+ }
+
+ foreach ($rl in $artifacts.ReceiveLocations) {
+ if (-not $maps.ReceiveLocations.ContainsKey($rl.Name)) { Write-Log "Receive location not found: $($rl.Name)" 'WARN'; continue }
+ $obj = $maps.ReceiveLocations[$rl.Name]
+ if ($rl.Enabled -eq $true) {
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Enable' -Action "Enable receive location '$($rl.Name)'" -PlanOnly:$PlanOnly
+ } else {
+ Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Disable' -Action "Keep receive location disabled '$($rl.Name)'" -PlanOnly:$PlanOnly
+ }
+ Wait-BizTalkCondition -Description "Receive location '$($rl.Name)' restored" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition {
+ $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_ReceiveLocation' -ComputerName $Server | Where-Object { $_.Name -eq $rl.Name } | Select-Object -First 1
+ $current -and ((-not $current.IsDisabled) -eq [bool]$rl.Enabled)
+ }
+ }
+}
+
+# ============================= HTML HELPERS =============================
+function New-Html-Header {
+ param([string]$Title)
+@"
+
+
+
+
+$Title
+
+
+$Title
+"@
+}
+
+function New-Html-Footer { "" }
+
+# ============================= SNAPSHOT =============================
+function Get-BizTalkSnapshot {
+ param(
+ [string]$OutputFile,
+ [string]$Server,
+ [string]$MgmtDbServer,
+ [string]$MgmtDbName
+ )
+
+ Write-Log "Snapshot: Server=$Server File=$OutputFile"
+
+ [void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.BizTalk.ExplorerOM')
+ $db = Get-BizTalkMgmtDbInfo -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName
+
+ $cat = New-Object Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer
+ $cat.ConnectionString = "Server={0};Database={1};Integrated Security=SSPI" -f $db.Server,$db.Name
+
+ $apps = $cat.Applications
+
+ # WMI lookup tables
+ $rlW = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_ReceiveLocation' -ComputerName $Server |
+ Select-Object Name, @{n='Enabled';e={-not $_.IsDisabled}}, AdapterName, @{n='Address';e={$_.InboundTransportURL}}
+
+ $spW = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server |
+ Select-Object Name, @{n='Status';e={$_.Status}}, @{n='PrimaryTransportType';e={$_.PTTransportType}}, @{n='PrimaryTransportAddress';e={$_.PTAddress}}
+
+ $orW = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_Orchestration' -ComputerName $Server |
+ Select-Object Name, @{n='OrchestrationStatus';e={$_.OrchestrationStatus}}
+
+ $mapRL=@{}; foreach($i in $rlW){$mapRL[$i.Name]=$i}
+ $mapSP=@{}; foreach($i in $spW){$mapSP[$i.Name]=$i}
+ $mapOR=@{}; foreach($i in $orW){$mapOR[$i.Name]=$i}
+
+ $snapshot=@()
+
+ foreach ($app in $apps) {
+ $rlItems=@()
+ foreach ($rp in $app.ReceivePorts) {
+ foreach ($rl in $rp.ReceiveLocations) {
+ if ($mapRL.ContainsKey($rl.Name)) {
+ $w = $mapRL[$rl.Name]
+ $rlItems += [pscustomobject]@{
+ Name=$rl.Name
+ Enabled=$w.Enabled
+ AdapterName=$w.AdapterName
+ Address=$w.Address
+ }
+ }
+ }
+ }
+
+ $spItems=@()
+ foreach ($sp in $app.SendPorts) {
+ if ($mapSP.ContainsKey($sp.Name)) {
+ $w = $mapSP[$sp.Name]
+ $spItems += [pscustomobject]@{
+ Name=$sp.Name
+ Status=$w.Status
+ PrimaryTransportType=$w.PrimaryTransportType
+ PrimaryTransportAddress=$w.PrimaryTransportAddress
+ }
+ }
+ }
+
+ $orItems=@()
+ foreach ($o in $app.Orchestrations) {
+ $key = if ($mapOR.ContainsKey($o.FullName)) { $o.FullName } else { $o.Name }
+ if ($mapOR.ContainsKey($key)) {
+ $w = $mapOR[$key]
+ $orItems += [pscustomobject]@{
+ Name=$o.FullName
+ OrchestrationStatus=$w.OrchestrationStatus
+ }
+ }
+ }
+
+ $snapshot += [pscustomobject]@{
+ Application=$app.Name
+ ReceiveLocations=$rlItems
+ SendPorts=$spItems
+ Orchestrations=$orItems
+ }
+ }
+
+ # Host Instances
+ $hostInstances = Get-BizTalkHostInstances -Server $Server
+
+ # Export JSON (Applications + HostInstances)
+ $full = [pscustomobject]@{
+ Applications = $snapshot
+ HostInstances = $hostInstances
+ }
+
+ $full | ConvertTo-Json -Depth 8 | Out-File $OutputFile -Encoding UTF8
+
+ # Export CSV (Artifacts)
+ $csvArtifact = foreach ($app in $snapshot) {
+ foreach ($rl in $app.ReceiveLocations) {
+ $st = Format-Status -ArtifactType 'ReceiveLocation' -Value $rl.Enabled
+ [pscustomobject]@{
+ Application=$app.Application
+ Type='ReceiveLocation'
+ Name=$rl.Name
+ Status=$st.Text
+ }
+ }
+ foreach ($sp in $app.SendPorts) {
+ $st = Format-Status -ArtifactType 'SendPort' -Value $sp.Status
+ [pscustomobject]@{
+ Application=$app.Application
+ Type='SendPort'
+ Name=$sp.Name
+ Status=$st.Text
+ }
+ }
+ foreach ($o in $app.Orchestrations) {
+ $st = Format-Status -ArtifactType 'Orchestration' -Value $o.OrchestrationStatus
+ [pscustomobject]@{
+ Application=$app.Application
+ Type='Orchestration'
+ Name=$o.Name
+ Status=$st.Text
+ }
+ }
+ }
+
+ $csvArtifact |
+ Export-Csv -Encoding UTF8 -NoTypeInformation -Path ($OutputFile + '.csv')
+
+ # Export CSV (HostInstances)
+ $hostInstances |
+ Select-Object InstanceName,HostName,Server,StateText |
+ Export-Csv -NoTypeInformation -Encoding UTF8 -Path ($OutputFile + '.hosts.csv')
+
+ # HTML Snapshot (modernisiert)
+ $html = @()
+ $html += New-Html-Header -Title 'BizTalk Snapshot'
+
+ # Summary
+ $totalApps = ($snapshot | Measure-Object).Count
+ $totalRL = ($snapshot.ReceiveLocations | Measure-Object).Count
+ $totalSP = ($snapshot.SendPorts | Measure-Object).Count
+ $totalOR = ($snapshot.Orchestrations | Measure-Object).Count
+ $totalHI = ($hostInstances | Measure-Object).Count
+
+ $html += ""
+ $html += ""
+ $html += "Receive Locations
$totalRL
"
+ $html += ""
+ $html += ""
+ $html += ""
+ $html += ""
+
+ foreach ($app in $snapshot) {
+ $html += ""
+ $html += "Application: $($app.Application)
"
+
+ if ($app.ReceiveLocations.Count -gt 0) {
+ $html += "Receive Locations
| Name | Status | Adapter | Address |
"
+ foreach ($rl in $app.ReceiveLocations) {
+ $st = Format-Status -ArtifactType 'ReceiveLocation' -Value $rl.Enabled
+ $html += "| $($rl.Name) | $($st.Text) | $($rl.AdapterName) | $($rl.Address) |
"
+ }
+ $html += "
"
+ }
+
+ if ($app.SendPorts.Count -gt 0) {
+ $html += "Send Ports
| Name | Status | PrimaryTransportType | PrimaryTransportAddress |
"
+ foreach ($sp in $app.SendPorts) {
+ $st = Format-Status -ArtifactType 'SendPort' -Value $sp.Status
+ $html += "| $($sp.Name) | $($st.Text) | $($sp.PrimaryTransportType) | $($sp.PrimaryTransportAddress) |
"
+ }
+ $html += "
"
+ }
+
+ if ($app.Orchestrations.Count -gt 0) {
+ $html += "Orchestrations
| Name | Status |
"
+ foreach ($o in $app.Orchestrations) {
+ $st = Format-Status -ArtifactType 'Orchestration' -Value $o.OrchestrationStatus
+ $html += "| $($o.Name) | $($st.Text) |
"
+ }
+ $html += "
"
+ }
+ $html += ""
+ }
+
+ # Host Instances
+ $html += ""
+ $html += "Host Instances
"
+ $html += "| Instance | Host | Server | Status |
"
+ foreach ($hi in $hostInstances) {
+ $html += "| $($hi.InstanceName) | $($hi.HostName) | $($hi.Server) | $($hi.StateText) |
"
+ }
+ $html += "
"
+ $html += ""
+
+ $html += New-Html-Footer
+ $html -join "`n" | Out-File ($OutputFile + '.html') -Encoding UTF8
+
+ # Console Output (wie Original)
+ Write-Host "`n===== Snapshot =====" -ForegroundColor Yellow
+ foreach ($app in $snapshot) {
+ Write-Host "`nApplication: $($app.Application)" -ForegroundColor Cyan
+
+ foreach ($rl in $app.ReceiveLocations) {
+ $st = Format-Status -ArtifactType 'ReceiveLocation' -Value $rl.Enabled
+ Write-Host (" RL {0,-60} {1}" -f $rl.Name,$st.Text) -ForegroundColor $st.ConsoleColor
+ }
+
+ foreach ($sp in $app.SendPorts) {
+ $st = Format-Status -ArtifactType 'SendPort' -Value $sp.Status
+ Write-Host (" SP {0,-60} {1}" -f $sp.Name,$st.Text) -ForegroundColor $st.ConsoleColor
+ }
+
+ foreach ($o in $app.Orchestrations) {
+ $st = Format-Status -ArtifactType 'Orchestration' -Value $o.OrchestrationStatus
+ Write-Host (" OR {0,-60} {1}" -f $o.Name,$st.Text) -ForegroundColor $st.ConsoleColor
+ }
+ }
+
+ # Host Instances (Konsole)
+ Write-Host "`n=== Host Instances ===" -ForegroundColor Yellow
+ foreach ($hi in $hostInstances) {
+ Write-Host ("{0,-35} {1,-20} {2,-10}" -f $hi.InstanceName,$hi.HostName,$hi.StateText) -ForegroundColor $hi.ConsoleColor
+ }
+
+ Write-Host "Snapshot erstellt: $(Resolve-Path $OutputFile)" -ForegroundColor Green
+}
+
+# ============================= COMPARE =============================
+function Compare-BizTalkSnapshots {
+ param(
+ [string]$Before,
+ [string]$After,
+ [string]$HtmlReport = 'BizTalkDiff.html'
+ )
+
+ Write-Log "Vergleiche Snapshots: $Before ↔ $After"
+
+ $b = Get-Content $Before | ConvertFrom-Json
+ $a = Get-Content $After | ConvertFrom-Json
+
+ $diff = New-Object System.Collections.ArrayList
+
+ function AddDiff {
+ param($app,$type,$name,$before,$after)
+ $bf = Format-Status -ArtifactType $type -Value $before
+ $af = Format-Status -ArtifactType $type -Value $after
+ [void]$diff.Add([pscustomobject]@{
+ Application = $app
+ ArtifactType= $type
+ Name = $name
+ BeforeText = $bf.Text
+ BeforeClass = $bf.Class
+ AfterText = $af.Text
+ AfterClass = $af.Class
+ })
+ }
+
+ foreach ($appAfter in $a.Applications) {
+ $appBefore = $b.Applications | Where-Object {$_.Application -eq $appAfter.Application}
+
+ # RECEIVE LOCATIONS
+ foreach ($x in $appAfter.ReceiveLocations) {
+ $old = $appBefore.ReceiveLocations | Where-Object {$_.Name -eq $x.Name}
+ if (-not $old) { AddDiff $appAfter.Application 'ReceiveLocation' $x.Name $null $x.Enabled; continue }
+ if ($old.Enabled -ne $x.Enabled) { AddDiff $appAfter.Application 'ReceiveLocation' $x.Name $old.Enabled $x.Enabled }
+ }
+ foreach ($x in $appBefore.ReceiveLocations) {
+ if (-not ($appAfter.ReceiveLocations | Where-Object {$_.Name -eq $x.Name})) {
+ AddDiff $appBefore.Application 'ReceiveLocation' $x.Name $x.Enabled $null
+ }
+ }
+
+ # SEND PORTS
+ foreach ($x in $appAfter.SendPorts) {
+ $old = $appBefore.SendPorts | Where-Object {$_.Name -eq $x.Name}
+ if (-not $old) { AddDiff $appAfter.Application 'SendPort' $x.Name $null $x.Status; continue }
+ if ($old.Status -ne $x.Status) { AddDiff $appAfter.Application 'SendPort' $x.Name $old.Status $x.Status }
+ }
+ foreach ($x in $appBefore.SendPorts) {
+ if (-not ($appAfter.SendPorts | Where-Object {$_.Name -eq $x.Name})) {
+ AddDiff $appBefore.Application 'SendPort' $x.Name $x.Status $null
+ }
+ }
+
+ # ORCHESTRATIONS
+ foreach ($x in $appAfter.Orchestrations) {
+ $old = $appBefore.Orchestrations | Where-Object {$_.Name -eq $x.Name}
+ if (-not $old) { AddDiff $appAfter.Application 'Orchestration' $x.Name $null $x.OrchestrationStatus; continue }
+ if ($old.OrchestrationStatus -ne $x.OrchestrationStatus) {
+ AddDiff $appAfter.Application 'Orchestration' $x.Name $old.OrchestrationStatus $x.OrchestrationStatus
+ }
+ }
+ foreach ($x in $appBefore.Orchestrations) {
+ if (-not ($appAfter.Orchestrations | Where-Object {$_.Name -eq $x.Name})) {
+ AddDiff $appBefore.Application 'Orchestration' $x.Name $x.OrchestrationStatus $null
+ }
+ }
+ }
+
+ # ======================== HOST INSTANCE DIFF ========================
+ $beforeHI = $b.HostInstances
+ $afterHI = $a.HostInstances
+
+ $hiDiff_New=@()
+ $hiDiff_Removed=@()
+ $hiDiff_Changed=@()
+
+ foreach ($hi in $afterHI) {
+ if (-not ($beforeHI | Where-Object {$_.InstanceName -eq $hi.InstanceName})) {
+ $hiDiff_New += $hi
+ }
+ }
+
+ foreach ($hi in $beforeHI) {
+ if (-not ($afterHI | Where-Object {$_.InstanceName -eq $hi.InstanceName})) {
+ $hiDiff_Removed += $hi
+ }
+ }
+
+ foreach ($hiAfter in $afterHI) {
+ $hiBefore = $beforeHI | Where-Object {$_.InstanceName -eq $hiAfter.InstanceName}
+ if ($hiBefore) {
+ if ($hiBefore.StateText -ne $hiAfter.StateText) {
+ $hiDiff_Changed += [pscustomobject]@{
+ Instance = $hiAfter.InstanceName
+ Before = $hiBefore.StateText
+ After = $hiAfter.StateText
+ }
+ }
+ }
+ }
+
+ # ======================== JSON DIFF EXPORT ========================
+ $jsonDiff = [pscustomobject]@{
+ ArtifactsDiff = $diff
+ HostInstances = [pscustomobject]@{
+ New = $hiDiff_New
+ Removed = $hiDiff_Removed
+ Changed = $hiDiff_Changed
+ }
+ }
+
+ $jsonDiff | ConvertTo-Json -Depth 8 | Out-File 'diff.json' -Encoding UTF8
+
+ # ======================== CSV DIFF EXPORT =========================
+ $diff | Export-Csv -Path 'diff.csv' -NoTypeInformation -Encoding UTF8
+ # HostInstance CSVs separat bereitstellen
+ $hiDiff_New | Export-Csv -Path 'diff.hosts.new.csv' -NoTypeInformation -Encoding UTF8
+ $hiDiff_Removed | Export-Csv -Path 'diff.hosts.removed.csv' -NoTypeInformation -Encoding UTF8
+ $hiDiff_Changed | Export-Csv -Path 'diff.hosts.changed.csv' -NoTypeInformation -Encoding UTF8
+
+ # ======================== HTML DIFF EXPORT ========================
+ $html=@()
+ $html+= New-Html-Header -Title 'BizTalk Differences'
+
+ # Summary-Karten
+ $artCount = ($diff | Measure-Object).Count
+ $hiNew = ($hiDiff_New | Measure-Object).Count
+ $hiRem = ($hiDiff_Removed | Measure-Object).Count
+ $hiChg = ($hiDiff_Changed | Measure-Object).Count
+
+ $html += ""
+ $html += "Artifact Differences
$artCount
"
+ $html += ""
+ $html += ""
+ $html += ""
+ $html += ""
+
+ # Artifacts
+ $html += ""
+ $html += "Artifact Differences
"
+ $html += "| Application | Type | Name | Before | After |
"
+ foreach ($d in $diff) {
+ $html += "| $($d.Application) | $($d.ArtifactType) | $($d.Name) | $($d.BeforeText) | $($d.AfterText) |
"
+ }
+ $html += "
"
+ $html += ""
+
+ # HostInstance: NEW
+ $html += ""
+ $html += "Host Instance Changes
"
+ $html += "New
| Instance | Host | Server | Status |
"
+ foreach ($hi in $hiDiff_New) {
+ $html += "| $($hi.InstanceName) | $($hi.HostName) | $($hi.Server) | $($hi.StateText) |
"
+ }
+ $html += "
"
+
+ # HostInstance: REMOVED
+ $html += "Removed
| Instance | Host | Server | Last Status |
"
+ foreach ($hi in $hiDiff_Removed) {
+ $html += "| $($hi.InstanceName) | $($hi.HostName) | $($hi.Server) | $($hi.StateText) |
"
+ }
+ $html += "
"
+
+ # HostInstance: CHANGED
+ $html += "Changed
| Instance | Before | After |
"
+ foreach ($hi in $hiDiff_Changed) {
+ $before = Format-Status -ArtifactType 'HostInstance' -Value $hi.Before
+ $after = Format-Status -ArtifactType 'HostInstance' -Value $hi.After
+ $html += "| $($hi.Instance) | $($hi.Before) | $($hi.After) |
"
+ }
+ $html += "
"
+ $html += ""
+
+ $html += New-Html-Footer
+ $html -join "`n" | Out-File $HtmlReport -Encoding UTF8
+
+ # ======================== CONSOLE OUTPUT =========================
+ Write-Host "`n===== BizTalk DIFF =====" -ForegroundColor Yellow
+
+ if ($diff.Count -eq 0 -and $hiDiff_New.Count -eq 0 -and $hiDiff_Removed.Count -eq 0 -and $hiDiff_Changed.Count -eq 0) {
+ Write-Host "Keine Unterschiede." -ForegroundColor Green
+ }
+ else {
+ Write-Host "`n--- Artifacts ---" -ForegroundColor Cyan
+ foreach ($d in $diff) {
+ Write-Host "`nApplication: $($d.Application)" -ForegroundColor Cyan
+ Write-Host "Artifact: $($d.ArtifactType)"
+ Write-Host "Name: $($d.Name)"
+ Write-Host ("Before: {0}" -f $d.BeforeText) -ForegroundColor $(if ($d.BeforeClass -eq 'ok') {'Green'} elseif ($d.BeforeClass -eq 'warn') {'Yellow'} else {'Red'})
+ Write-Host ("After: {0}" -f $d.AfterText) -ForegroundColor $(if ($d.AfterClass -eq 'ok') {'Green'} elseif ($d.AfterClass -eq 'warn') {'Yellow'} else {'Red'})
+ }
+
+ Write-Host "`n--- Host Instances (New) ---" -ForegroundColor Cyan
+ foreach ($hi in $hiDiff_New) {
+ Write-Host ("NEW: {0} {1}" -f $hi.InstanceName,$hi.StateText) -ForegroundColor $hi.ConsoleColor
+ }
+
+ Write-Host "`n--- Host Instances (Removed) ---" -ForegroundColor Cyan
+ foreach ($hi in $hiDiff_Removed) {
+ Write-Host ("REMOVED: {0} {1}" -f $hi.InstanceName,$hi.StateText) -ForegroundColor Red
+ }
+
+ Write-Host "`n--- Host Instances (Changed) ---" -ForegroundColor Cyan
+ foreach ($hi in $hiDiff_Changed) {
+ Write-Host ("CHANGED: {0} {1} -> {2}" -f $hi.Instance,$hi.Before,$hi.After) -ForegroundColor Yellow
+ }
+ }
+
+ Write-Host "HTML Diff exportiert: $HtmlReport" -ForegroundColor Green
+}
+
+# ============================= DISPATCH =============================
+try {
+ if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
+ Set-Location $OutDir
+ Write-Log "Working dir: $(Get-Location)"
+
+ if ($Diag) {
+ Write-Host "DIAG → Server: $Server" -ForegroundColor Yellow
+ try {
+ $sp = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server | Select-Object -First 3 Name,Status
+ Write-Host "WMI/CIM OK:" -ForegroundColor Green
+ $sp | Format-Table
+ } catch {
+ Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
+ }
+ exit
+ }
+
+ if ($Shutdown) {
+ Get-BizTalkSnapshot -OutputFile $StateFile -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName
+ $snapshot = Get-BizTalkStateFile -Path $StateFile
+ Stop-BizTalkRuntimeFromSnapshot -Snapshot $snapshot -Server $Server -WaitTimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly
+ if (-not $PlanOnly) {
+ Get-BizTalkSnapshot -OutputFile 'shutdown-after.json' -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName
+ }
+ exit
+ }
+
+ if ($Restore) {
+ $snapshot = Get-BizTalkStateFile -Path $StateFile
+ Restore-BizTalkRuntimeFromSnapshot -Snapshot $snapshot -Server $Server -WaitTimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly
+ if (-not $PlanOnly) {
+ Get-BizTalkSnapshot -OutputFile 'restore-after.json' -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName
+ }
+ exit
+ }
+
+ if ($Before) { Get-BizTalkSnapshot -OutputFile 'before.json' -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName; exit }
+ if ($After) { Get-BizTalkSnapshot -OutputFile 'after.json' -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName; exit }
+ if ($Compare){ Compare-BizTalkSnapshots -Before 'before.json' -After 'after.json'; exit }
+
+ Write-Host "Usage:"
+ Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Before"
+ Write-Host " .\\BizTalkPlatformManagementTool.ps1 -After"
+ Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Compare"
+ Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Diag"
+ Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Shutdown [-PlanOnly] [-StateFile before.json]"
+ Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Restore [-PlanOnly] [-StateFile before.json]"
+}
+catch {
+ Write-Log "Fatal: $($_.Exception.Message)" 'ERROR'
+ throw
+}
diff --git a/src/BizTalkPlatformManagementTool/App.config b/src/BizTalkPlatformManagementTool/App.config
new file mode 100644
index 0000000..f401e47
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj b/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj
new file mode 100644
index 0000000..47ecbce
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj
@@ -0,0 +1,67 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}
+ WinExe
+ BizTalkPlatformManagementTool
+ BizTalkPlatformManagementTool
+ v4.7.1
+ 512
+ true
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs b/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs
new file mode 100644
index 0000000..6809fa3
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs
@@ -0,0 +1,59 @@
+namespace BizTalkPlatformManagementTool.Models
+{
+ public static class ArtifactStates
+ {
+ public const int SendPortBound = 1;
+ public const int SendPortStopped = 2;
+ public const int SendPortStarted = 3;
+
+ public const int OrchestrationUnbound = 1;
+ public const int OrchestrationBound = 2;
+ public const int OrchestrationStopped = 3;
+ public const int OrchestrationStarted = 4;
+
+ public const int HostStopped = 1;
+ public const int HostStartPending = 2;
+ public const int HostStopPending = 3;
+ public const int HostStarted = 4;
+
+ public static string FormatReceiveLocation(bool enabled)
+ {
+ return enabled ? "Enabled" : "Disabled";
+ }
+
+ public static string FormatSendPort(int status)
+ {
+ switch (status)
+ {
+ case SendPortStarted: return "Started";
+ case SendPortStopped: return "Stopped";
+ case SendPortBound: return "Bound";
+ default: return "Unknown (" + status + ")";
+ }
+ }
+
+ public static string FormatOrchestration(int status)
+ {
+ switch (status)
+ {
+ case OrchestrationStarted: return "Started";
+ case OrchestrationStopped: return "Stopped";
+ case OrchestrationBound: return "Bound";
+ case OrchestrationUnbound: return "Unbound";
+ default: return "Unknown (" + status + ")";
+ }
+ }
+
+ public static string FormatHostInstance(int state)
+ {
+ switch (state)
+ {
+ case HostStarted: return "Started";
+ case HostStartPending: return "StartPending";
+ case HostStopPending: return "StopPending";
+ case HostStopped: return "Stopped";
+ default: return "Unknown (" + state + ")";
+ }
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs b/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs
new file mode 100644
index 0000000..d769252
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs
@@ -0,0 +1,126 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace BizTalkPlatformManagementTool.Models
+{
+ [DataContract]
+ public sealed class BizTalkSnapshot
+ {
+ public BizTalkSnapshot()
+ {
+ Applications = new List();
+ HostInstances = new List();
+ }
+
+ [DataMember(Order = 1)]
+ public string ToolVersion { get; set; }
+
+ [DataMember(Order = 2)]
+ public string CreatedAt { get; set; }
+
+ [DataMember(Order = 3)]
+ public string Server { get; set; }
+
+ [DataMember(Order = 4)]
+ public List Applications { get; set; }
+
+ [DataMember(Order = 5)]
+ public List HostInstances { get; set; }
+ }
+
+ [DataContract]
+ public sealed class ApplicationSnapshot
+ {
+ public ApplicationSnapshot()
+ {
+ ReceiveLocations = new List();
+ SendPorts = new List();
+ Orchestrations = new List();
+ }
+
+ [DataMember(Order = 1)]
+ public string Application { get; set; }
+
+ [DataMember(Order = 2)]
+ public List ReceiveLocations { get; set; }
+
+ [DataMember(Order = 3)]
+ public List SendPorts { get; set; }
+
+ [DataMember(Order = 4)]
+ public List Orchestrations { get; set; }
+ }
+
+ [DataContract]
+ public sealed class ReceiveLocationState
+ {
+ [DataMember(Order = 1)]
+ public string Application { get; set; }
+
+ [DataMember(Order = 2)]
+ public string Name { get; set; }
+
+ [DataMember(Order = 3)]
+ public string ReceivePortName { get; set; }
+
+ [DataMember(Order = 4)]
+ public bool Enabled { get; set; }
+
+ [DataMember(Order = 5)]
+ public string AdapterName { get; set; }
+
+ [DataMember(Order = 6)]
+ public string Address { get; set; }
+ }
+
+ [DataContract]
+ public sealed class SendPortState
+ {
+ [DataMember(Order = 1)]
+ public string Application { get; set; }
+
+ [DataMember(Order = 2)]
+ public string Name { get; set; }
+
+ [DataMember(Order = 3)]
+ public int Status { get; set; }
+
+ [DataMember(Order = 4)]
+ public string PrimaryTransportType { get; set; }
+
+ [DataMember(Order = 5)]
+ public string PrimaryTransportAddress { get; set; }
+ }
+
+ [DataContract]
+ public sealed class OrchestrationState
+ {
+ [DataMember(Order = 1)]
+ public string Application { get; set; }
+
+ [DataMember(Order = 2)]
+ public string Name { get; set; }
+
+ [DataMember(Order = 3)]
+ public int OrchestrationStatus { get; set; }
+ }
+
+ [DataContract]
+ public sealed class HostInstanceState
+ {
+ [DataMember(Order = 1)]
+ public string InstanceName { get; set; }
+
+ [DataMember(Order = 2)]
+ public string HostName { get; set; }
+
+ [DataMember(Order = 3)]
+ public string Server { get; set; }
+
+ [DataMember(Order = 4)]
+ public int RawState { get; set; }
+
+ [DataMember(Order = 5)]
+ public string StateText { get; set; }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Models/DiffModels.cs b/src/BizTalkPlatformManagementTool/Models/DiffModels.cs
new file mode 100644
index 0000000..f4a19d4
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Models/DiffModels.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace BizTalkPlatformManagementTool.Models
+{
+ [DataContract]
+ public sealed class SnapshotDiff
+ {
+ public SnapshotDiff()
+ {
+ ArtifactDifferences = new List();
+ HostInstanceDifferences = new List();
+ }
+
+ [DataMember(Order = 1)]
+ public List ArtifactDifferences { get; set; }
+
+ [DataMember(Order = 2)]
+ public List HostInstanceDifferences { get; set; }
+ }
+
+ [DataContract]
+ public sealed class ArtifactDiffEntry
+ {
+ [DataMember(Order = 1)]
+ public string Application { get; set; }
+
+ [DataMember(Order = 2)]
+ public string ArtifactType { get; set; }
+
+ [DataMember(Order = 3)]
+ public string Name { get; set; }
+
+ [DataMember(Order = 4)]
+ public string Before { get; set; }
+
+ [DataMember(Order = 5)]
+ public string After { get; set; }
+ }
+
+ [DataContract]
+ public sealed class HostInstanceDiffEntry
+ {
+ [DataMember(Order = 1)]
+ public string InstanceName { get; set; }
+
+ [DataMember(Order = 2)]
+ public string HostName { get; set; }
+
+ [DataMember(Order = 3)]
+ public string Server { get; set; }
+
+ [DataMember(Order = 4)]
+ public string Before { get; set; }
+
+ [DataMember(Order = 5)]
+ public string After { get; set; }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Models/OperationModels.cs b/src/BizTalkPlatformManagementTool/Models/OperationModels.cs
new file mode 100644
index 0000000..0ff62a4
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Models/OperationModels.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace BizTalkPlatformManagementTool.Models
+{
+ public enum OperationMode
+ {
+ Shutdown,
+ Restore
+ }
+
+ public enum OperationStepKind
+ {
+ ReceiveLocation,
+ SendPort,
+ Orchestration,
+ HostInstance,
+ Note
+ }
+
+ [DataContract]
+ public sealed class OperationPlan
+ {
+ public OperationPlan()
+ {
+ Steps = new List();
+ }
+
+ [DataMember(Order = 1)]
+ public string Mode { get; set; }
+
+ [DataMember(Order = 2)]
+ public string CreatedAt { get; set; }
+
+ [DataMember(Order = 3)]
+ public string Server { get; set; }
+
+ [DataMember(Order = 4)]
+ public List Steps { get; set; }
+ }
+
+ [DataContract]
+ public sealed class OperationStep
+ {
+ [DataMember(Order = 1)]
+ public string Kind { get; set; }
+
+ [DataMember(Order = 2)]
+ public string Application { get; set; }
+
+ [DataMember(Order = 3)]
+ public string Name { get; set; }
+
+ [DataMember(Order = 4)]
+ public string Server { get; set; }
+
+ [DataMember(Order = 5)]
+ public string Action { get; set; }
+
+ [DataMember(Order = 6)]
+ public string WmiClass { get; set; }
+
+ [DataMember(Order = 7)]
+ public string KeyProperty { get; set; }
+
+ [DataMember(Order = 8)]
+ public string KeyValue { get; set; }
+
+ [DataMember(Order = 9)]
+ public string MethodName { get; set; }
+
+ [DataMember(Order = 10)]
+ public int[] Arguments { get; set; }
+
+ [DataMember(Order = 11)]
+ public int? TargetState { get; set; }
+
+ [DataMember(Order = 12)]
+ public bool Execute { get; set; }
+
+ [DataMember(Order = 13)]
+ public string Warning { get; set; }
+ }
+
+ public sealed class OperationOptions
+ {
+ public string Server { get; set; }
+ public string OutputDirectory { get; set; }
+ public string StateFile { get; set; }
+ public bool DryRun { get; set; }
+ public int WaitTimeoutSeconds { get; set; }
+ public int PollIntervalSeconds { get; set; }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Program.cs b/src/BizTalkPlatformManagementTool/Program.cs
new file mode 100644
index 0000000..e25e27a
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Program.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Windows.Forms;
+using BizTalkPlatformManagementTool.Ui;
+
+namespace BizTalkPlatformManagementTool
+{
+ internal static class Program
+ {
+ [STAThread]
+ private static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.Run(new MainForm());
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs
new file mode 100644
index 0000000..5667699
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs
@@ -0,0 +1,415 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Management;
+using BizTalkPlatformManagementTool.Models;
+
+namespace BizTalkPlatformManagementTool.Services
+{
+ public sealed class BizTalkOperationService
+ {
+ public const string Version = "2.0.0-net471";
+ private const string UnknownApplication = "(Unknown Application)";
+
+ private readonly OperationLogger _logger;
+
+ public BizTalkOperationService(OperationLogger logger)
+ {
+ _logger = logger;
+ }
+
+ public void Diagnose(string server)
+ {
+ using (var client = CreateClient(server))
+ {
+ var ports = client.Query("MSBTS_SendPort");
+ _logger.Success("WMI/CIM diagnostic succeeded. Send ports visible: " + ports.Count);
+ }
+ }
+
+ public BizTalkSnapshot CreateSnapshot(string server)
+ {
+ using (var client = CreateClient(server))
+ {
+ var snapshot = new BizTalkSnapshot
+ {
+ ToolVersion = Version,
+ CreatedAt = DateTime.Now.ToString("s"),
+ Server = client.Server
+ };
+
+ var apps = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var item in client.Query("MSBTS_ReceiveLocation"))
+ {
+ var appName = GetApplicationName(item);
+ var app = GetApplication(apps, appName);
+ app.ReceiveLocations.Add(new ReceiveLocationState
+ {
+ Application = app.Application,
+ Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
+ ReceivePortName = BizTalkWmiClient.SafeGetString(item, "ReceivePortName", string.Empty),
+ Enabled = !BizTalkWmiClient.SafeGetBoolean(item, "IsDisabled", true),
+ AdapterName = BizTalkWmiClient.SafeGetString(item, "AdapterName", string.Empty),
+ Address = BizTalkWmiClient.SafeGetString(item, "InboundTransportURL", string.Empty)
+ });
+ }
+
+ foreach (var item in client.Query("MSBTS_SendPort"))
+ {
+ var appName = GetApplicationName(item);
+ var app = GetApplication(apps, appName);
+ app.SendPorts.Add(new SendPortState
+ {
+ Application = app.Application,
+ Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
+ Status = BizTalkWmiClient.SafeGetInt32(item, "Status", 0),
+ PrimaryTransportType = BizTalkWmiClient.SafeGetString(item, "PTTransportType", string.Empty),
+ PrimaryTransportAddress = BizTalkWmiClient.SafeGetString(item, "PTAddress", string.Empty)
+ });
+ }
+
+ foreach (var item in client.Query("MSBTS_Orchestration"))
+ {
+ var appName = GetApplicationName(item);
+ var app = GetApplication(apps, appName);
+ app.Orchestrations.Add(new OrchestrationState
+ {
+ Application = app.Application,
+ Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
+ OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0)
+ });
+ }
+
+ foreach (var item in client.Query("MSBTS_HostInstance"))
+ {
+ var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0);
+ snapshot.HostInstances.Add(new HostInstanceState
+ {
+ InstanceName = BizTalkWmiClient.SafeGetString(item, "InstanceName", BizTalkWmiClient.SafeGetString(item, "Name", string.Empty)),
+ HostName = BizTalkWmiClient.SafeGetString(item, "HostName", string.Empty),
+ Server = BizTalkWmiClient.SafeGetString(item, "RunningServer", string.Empty),
+ RawState = state,
+ StateText = ArtifactStates.FormatHostInstance(state)
+ });
+ }
+
+ foreach (var app in apps.Values.OrderBy(a => a.Application))
+ {
+ app.ReceiveLocations = app.ReceiveLocations.OrderBy(x => x.Name).ToList();
+ app.SendPorts = app.SendPorts.OrderBy(x => x.Name).ToList();
+ app.Orchestrations = app.Orchestrations.OrderBy(x => x.Name).ToList();
+ snapshot.Applications.Add(app);
+ }
+ snapshot.HostInstances = snapshot.HostInstances.OrderBy(x => x.Server).ThenBy(x => x.InstanceName).ToList();
+
+ _logger.Success("Snapshot created. Applications: " + snapshot.Applications.Count + ", host instances: " + snapshot.HostInstances.Count);
+ return snapshot;
+ }
+ }
+
+ public OperationPlan CreateShutdownPlan(BizTalkSnapshot snapshot, string server)
+ {
+ var plan = NewPlan(OperationMode.Shutdown, server);
+
+ foreach (var app in snapshot.Applications)
+ {
+ 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, false));
+ }
+
+ foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
+ {
+ plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Stop orchestration", "MSBTS_Orchestration", "Name", item.Name, "Stop", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped));
+ }
+
+ foreach (var item in app.SendPorts.Where(x => x.Status == ArtifactStates.SendPortStarted))
+ {
+ plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Stop send port", "MSBTS_SendPort", "Name", item.Name, "Stop", null, ArtifactStates.SendPortStopped));
+ }
+ }
+
+ foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
+ {
+ var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Stop host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Stop", null, ArtifactStates.HostStopped);
+ if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase))
+ {
+ step.Execute = false;
+ step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server.";
+ }
+ plan.Steps.Add(step);
+ }
+
+ return plan;
+ }
+
+ public OperationPlan CreateRestorePlan(BizTalkSnapshot snapshot, string server)
+ {
+ var plan = NewPlan(OperationMode.Restore, server);
+
+ foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
+ {
+ var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Start host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted);
+ if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase))
+ {
+ step.Execute = false;
+ step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server.";
+ }
+ plan.Steps.Add(step);
+ }
+
+ foreach (var app in snapshot.Applications)
+ {
+ foreach (var item in app.SendPorts)
+ {
+ if (item.Status == ArtifactStates.SendPortStarted)
+ {
+ plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Start send port", "MSBTS_SendPort", "Name", item.Name, "Start", null, ArtifactStates.SendPortStarted));
+ }
+ else if (item.Status == ArtifactStates.SendPortStopped)
+ {
+ plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Ensure send port is stopped", "MSBTS_SendPort", "Name", item.Name, "StopOrEnlist", null, ArtifactStates.SendPortStopped));
+ }
+ else if (item.Status == ArtifactStates.SendPortBound)
+ {
+ plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Ensure send port is bound", "MSBTS_SendPort", "Name", item.Name, "UnEnlist", null, ArtifactStates.SendPortBound));
+ }
+ }
+
+ foreach (var item in app.Orchestrations)
+ {
+ if (item.OrchestrationStatus == ArtifactStates.OrchestrationStarted)
+ {
+ plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Start orchestration", "MSBTS_Orchestration", "Name", item.Name, "Start", new[] { 1, 1, 1 }, ArtifactStates.OrchestrationStarted));
+ }
+ else if (item.OrchestrationStatus == ArtifactStates.OrchestrationStopped)
+ {
+ plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Ensure orchestration is stopped", "MSBTS_Orchestration", "Name", item.Name, "StopIfStarted", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped));
+ }
+ else if (item.OrchestrationStatus == ArtifactStates.OrchestrationBound)
+ {
+ plan.Steps.Add(new OperationStep
+ {
+ Kind = "Note",
+ Application = app.Application,
+ Name = item.Name,
+ Action = "Orchestration was Bound in the snapshot; leaving unchanged to avoid making it Unbound.",
+ Execute = false,
+ Warning = "Manual check recommended."
+ });
+ }
+ else if (item.OrchestrationStatus == ArtifactStates.OrchestrationUnbound)
+ {
+ plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Unenlist orchestration", "MSBTS_Orchestration", "Name", item.Name, "UnenlistService", new[] { 1 }, ArtifactStates.OrchestrationUnbound));
+ }
+ }
+
+ 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));
+ }
+ }
+
+ return plan;
+ }
+
+ public void ExecutePlan(OperationPlan plan, OperationOptions options)
+ {
+ using (var client = CreateClient(options.Server))
+ {
+ foreach (var step in plan.Steps)
+ {
+ if (!step.Execute)
+ {
+ _logger.Warning(step.Action + (string.IsNullOrEmpty(step.Warning) ? string.Empty : " " + step.Warning));
+ continue;
+ }
+
+ if (options.DryRun)
+ {
+ _logger.Info("DRY RUN: " + step.Action + " '" + step.Name + "'");
+ continue;
+ }
+
+ using (var instance = client.FindByProperty(step.WmiClass, step.KeyProperty, step.KeyValue))
+ {
+ if (instance == null)
+ {
+ throw new InvalidOperationException(step.Kind + " not found: " + step.Name);
+ }
+
+ ExecuteStep(client, instance, step, options);
+ }
+ }
+ }
+ }
+
+ public string SaveSnapshot(string outputDirectory, string fileName, BizTalkSnapshot snapshot)
+ {
+ Directory.CreateDirectory(outputDirectory);
+ var path = Path.Combine(outputDirectory, fileName);
+ SnapshotStore.SaveSnapshotSet(path, snapshot);
+ _logger.Success("Saved snapshot: " + path);
+ return path;
+ }
+
+ public string SavePlan(string outputDirectory, string fileName, OperationPlan plan)
+ {
+ Directory.CreateDirectory(outputDirectory);
+ var path = Path.Combine(outputDirectory, fileName);
+ JsonFileStore.Save(path, plan);
+ _logger.Success("Saved operation plan: " + path);
+ return path;
+ }
+
+ public string SaveDiff(string outputDirectory, string fileName, SnapshotDiff diff)
+ {
+ Directory.CreateDirectory(outputDirectory);
+ var path = Path.Combine(outputDirectory, fileName);
+ SnapshotStore.SaveDiffSet(path, diff);
+ _logger.Success("Saved diff: " + path);
+ return path;
+ }
+
+ private void ExecuteStep(BizTalkWmiClient client, ManagementObject instance, OperationStep step, OperationOptions options)
+ {
+ if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase))
+ {
+ var status = BizTalkWmiClient.SafeGetInt32(instance, "Status", 0);
+ if (status == ArtifactStates.SendPortBound)
+ {
+ client.InvokeMethod(instance, "Enlist");
+ }
+ else if (status == ArtifactStates.SendPortStarted)
+ {
+ client.InvokeMethod(instance, "Stop");
+ }
+ else
+ {
+ _logger.Info("Already stopped: " + step.Name);
+ }
+ }
+ else if (string.Equals(step.MethodName, "StopIfStarted", StringComparison.OrdinalIgnoreCase))
+ {
+ var status = BizTalkWmiClient.SafeGetInt32(instance, "OrchestrationStatus", 0);
+ if (status == ArtifactStates.OrchestrationStarted)
+ {
+ client.InvokeMethod(instance, "Stop", ToObjects(step.Arguments));
+ }
+ else
+ {
+ _logger.Info("No stop required: " + step.Name);
+ }
+ }
+ else
+ {
+ client.InvokeMethod(instance, step.MethodName, ToObjects(step.Arguments));
+ }
+
+ WaitForTarget(client, step, options);
+ }
+
+ private void WaitForTarget(BizTalkWmiClient client, OperationStep step, OperationOptions options)
+ {
+ if (!step.TargetState.HasValue)
+ {
+ if (step.Kind == "ReceiveLocation")
+ {
+ var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase);
+ client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => !BizTalkWmiClient.SafeGetBoolean(o, "IsDisabled", true) == shouldBeEnabled, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
+ }
+ return;
+ }
+
+ if (step.Kind == "SendPort")
+ {
+ client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "Status", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
+ }
+ else if (step.Kind == "Orchestration")
+ {
+ client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "OrchestrationStatus", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
+ }
+ else if (step.Kind == "HostInstance")
+ {
+ client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "ServiceState", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
+ }
+ }
+
+ private BizTalkWmiClient CreateClient(string server)
+ {
+ var client = new BizTalkWmiClient(server, _logger);
+ client.Connect();
+ return client;
+ }
+
+ private static OperationPlan NewPlan(OperationMode mode, string server)
+ {
+ return new OperationPlan
+ {
+ Mode = mode.ToString(),
+ CreatedAt = DateTime.Now.ToString("s"),
+ Server = server
+ };
+ }
+
+ private static OperationStep Step(string kind, string application, string name, string server, string action, string wmiClass, string keyProperty, string keyValue, string methodName, int[] arguments, int? targetState)
+ {
+ return new OperationStep
+ {
+ Kind = kind,
+ Application = application,
+ Name = name,
+ Server = server,
+ Action = action,
+ WmiClass = wmiClass,
+ KeyProperty = keyProperty,
+ KeyValue = keyValue,
+ MethodName = methodName,
+ Arguments = arguments,
+ TargetState = targetState,
+ Execute = true
+ };
+ }
+
+ private static object[] ToObjects(int[] values)
+ {
+ if (values == null || values.Length == 0)
+ {
+ return new object[0];
+ }
+
+ var result = new object[values.Length];
+ for (int i = 0; i < values.Length; i++)
+ {
+ result[i] = values[i];
+ }
+ return result;
+ }
+
+ private static ApplicationSnapshot GetApplication(Dictionary apps, string appName)
+ {
+ ApplicationSnapshot app;
+ if (!apps.TryGetValue(appName, out app))
+ {
+ app = new ApplicationSnapshot { Application = appName };
+ apps.Add(appName, app);
+ }
+ return app;
+ }
+
+ private static string GetApplicationName(ManagementObject item)
+ {
+ var names = new[] { "ApplicationName", "Application", "BizTalkApplication" };
+ foreach (var name in names)
+ {
+ var value = BizTalkWmiClient.SafeGetString(item, name, null);
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ return value;
+ }
+ }
+ return UnknownApplication;
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs b/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs
new file mode 100644
index 0000000..3523704
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Management;
+using System.Threading;
+
+namespace BizTalkPlatformManagementTool.Services
+{
+ public sealed class BizTalkWmiClient : IDisposable
+ {
+ private const string NamespacePath = "root\\MicrosoftBizTalkServer";
+ private readonly string _server;
+ private readonly OperationLogger _logger;
+ private ManagementScope _scope;
+
+ public BizTalkWmiClient(string server, OperationLogger logger)
+ {
+ _server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
+ _logger = logger;
+ }
+
+ public string Server
+ {
+ get { return _server; }
+ }
+
+ public void Connect()
+ {
+ var path = "\\\\" + _server + "\\" + NamespacePath;
+ _logger.Info("Connecting to WMI namespace " + path);
+ _scope = new ManagementScope(path);
+ _scope.Connect();
+ _logger.Success("WMI connection established.");
+ }
+
+ public List Query(string className)
+ {
+ EnsureConnected();
+
+ var result = new List();
+ var query = new ObjectQuery("SELECT * FROM " + className);
+ using (var searcher = new ManagementObjectSearcher(_scope, query))
+ using (var collection = searcher.Get())
+ {
+ foreach (ManagementObject item in collection)
+ {
+ result.Add(item);
+ }
+ }
+
+ return result;
+ }
+
+ public ManagementObject FindByProperty(string className, string propertyName, string value)
+ {
+ EnsureConnected();
+
+ var query = string.Format(
+ CultureInfo.InvariantCulture,
+ "SELECT * FROM {0} WHERE {1} = \"{2}\"",
+ className,
+ propertyName,
+ EscapeWql(value));
+
+ using (var searcher = new ManagementObjectSearcher(_scope, new ObjectQuery(query)))
+ using (var collection = searcher.Get())
+ {
+ foreach (ManagementObject item in collection)
+ {
+ return item;
+ }
+ }
+
+ return null;
+ }
+
+ public uint InvokeMethod(ManagementObject instance, string methodName, params object[] arguments)
+ {
+ if (instance == null)
+ {
+ throw new ArgumentNullException("instance");
+ }
+
+ var methodToCall = ResolveMethodName(instance, methodName);
+ _logger.Info("Calling " + instance.Path.ClassName + "." + methodToCall + " on " + SafeGetString(instance, "Name", SafeGetString(instance, "InstanceName", "")));
+
+ object result = instance.InvokeMethod(methodToCall, arguments == null || arguments.Length == 0 ? null : arguments);
+ var returnCode = ExtractReturnCode(result);
+ if (returnCode != 0)
+ {
+ throw new InvalidOperationException(methodToCall + " returned HRESULT/ReturnValue " + returnCode.ToString(CultureInfo.InvariantCulture));
+ }
+
+ return returnCode;
+ }
+
+ public void WaitForState(string className, string keyProperty, string keyValue, Func isReached, string description, int timeoutSeconds, int pollIntervalSeconds)
+ {
+ var deadline = DateTime.UtcNow.AddSeconds(Math.Max(1, timeoutSeconds));
+ var delay = Math.Max(1, pollIntervalSeconds);
+
+ while (DateTime.UtcNow <= deadline)
+ {
+ using (var current = FindByProperty(className, keyProperty, keyValue))
+ {
+ if (current != null && isReached(current))
+ {
+ _logger.Success("Reached: " + description);
+ return;
+ }
+ }
+
+ Thread.Sleep(TimeSpan.FromSeconds(delay));
+ }
+
+ throw new TimeoutException("Timeout while waiting for " + description);
+ }
+
+ public static string SafeGetString(ManagementBaseObject item, string propertyName, string fallback)
+ {
+ try
+ {
+ if (!HasProperty(item, propertyName))
+ {
+ return fallback;
+ }
+
+ var value = item[propertyName];
+ return value == null ? fallback : Convert.ToString(value, CultureInfo.InvariantCulture);
+ }
+ catch (ManagementException)
+ {
+ return fallback;
+ }
+ }
+
+ public static int SafeGetInt32(ManagementBaseObject item, string propertyName, int fallback)
+ {
+ try
+ {
+ if (!HasProperty(item, propertyName))
+ {
+ return fallback;
+ }
+
+ var value = item[propertyName];
+ return value == null ? fallback : Convert.ToInt32(value, CultureInfo.InvariantCulture);
+ }
+ catch
+ {
+ return fallback;
+ }
+ }
+
+ public static bool SafeGetBoolean(ManagementBaseObject item, string propertyName, bool fallback)
+ {
+ try
+ {
+ if (!HasProperty(item, propertyName))
+ {
+ return fallback;
+ }
+
+ var value = item[propertyName];
+ return value == null ? fallback : Convert.ToBoolean(value, CultureInfo.InvariantCulture);
+ }
+ catch
+ {
+ return fallback;
+ }
+ }
+
+ private void EnsureConnected()
+ {
+ if (_scope == null || !_scope.IsConnected)
+ {
+ Connect();
+ }
+ }
+
+ private static bool HasProperty(ManagementBaseObject item, string propertyName)
+ {
+ foreach (PropertyData property in item.Properties)
+ {
+ if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static string ResolveMethodName(ManagementObject instance, string requestedName)
+ {
+ foreach (MethodData method in instance.Methods)
+ {
+ if (string.Equals(method.Name, requestedName, StringComparison.OrdinalIgnoreCase))
+ {
+ return method.Name;
+ }
+ }
+
+ throw new MissingMethodException(instance.Path.ClassName, requestedName);
+ }
+
+ private static uint ExtractReturnCode(object result)
+ {
+ if (result == null)
+ {
+ return 0;
+ }
+
+ var outParams = result as ManagementBaseObject;
+ if (outParams != null && HasProperty(outParams, "ReturnValue"))
+ {
+ return Convert.ToUInt32(outParams["ReturnValue"], CultureInfo.InvariantCulture);
+ }
+
+ return Convert.ToUInt32(result, CultureInfo.InvariantCulture);
+ }
+
+ private static string EscapeWql(string value)
+ {
+ return (value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"");
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Services/CsvWriter.cs b/src/BizTalkPlatformManagementTool/Services/CsvWriter.cs
new file mode 100644
index 0000000..ec8b08c
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Services/CsvWriter.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using BizTalkPlatformManagementTool.Models;
+
+namespace BizTalkPlatformManagementTool.Services
+{
+ public static class CsvWriter
+ {
+ public static void WriteSnapshotArtifacts(string path, BizTalkSnapshot snapshot)
+ {
+ var lines = new List { "Application,Type,Name,Status" };
+ foreach (var app in snapshot.Applications)
+ {
+ foreach (var item in app.ReceiveLocations)
+ {
+ lines.Add(Row(app.Application, "ReceiveLocation", item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled)));
+ }
+ foreach (var item in app.SendPorts)
+ {
+ lines.Add(Row(app.Application, "SendPort", item.Name, ArtifactStates.FormatSendPort(item.Status)));
+ }
+ foreach (var item in app.Orchestrations)
+ {
+ lines.Add(Row(app.Application, "Orchestration", item.Name, ArtifactStates.FormatOrchestration(item.OrchestrationStatus)));
+ }
+ }
+ File.WriteAllLines(path, lines, Encoding.UTF8);
+ }
+
+ public static void WriteSnapshotHosts(string path, BizTalkSnapshot snapshot)
+ {
+ var lines = new List { "InstanceName,HostName,Server,State" };
+ foreach (var item in snapshot.HostInstances)
+ {
+ lines.Add(Row(item.InstanceName, item.HostName, item.Server, item.StateText));
+ }
+ File.WriteAllLines(path, lines, Encoding.UTF8);
+ }
+
+ public static void WriteDiff(string path, SnapshotDiff diff)
+ {
+ var lines = new List { "Scope,Application,Type,Name,Server,Before,After" };
+ foreach (var item in diff.ArtifactDifferences)
+ {
+ lines.Add(Row("Artifact", item.Application, item.ArtifactType, item.Name, string.Empty, item.Before, item.After));
+ }
+ foreach (var item in diff.HostInstanceDifferences)
+ {
+ lines.Add(Row("HostInstance", string.Empty, "HostInstance", item.InstanceName, item.Server, item.Before, item.After));
+ }
+ File.WriteAllLines(path, lines, Encoding.UTF8);
+ }
+
+ private static string Row(params string[] values)
+ {
+ var escaped = new string[values.Length];
+ for (int i = 0; i < values.Length; i++)
+ {
+ escaped[i] = Escape(values[i]);
+ }
+ return string.Join(",", escaped);
+ }
+
+ private static string Escape(string value)
+ {
+ value = value ?? string.Empty;
+ if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0)
+ {
+ return value;
+ }
+ return "\"" + value.Replace("\"", "\"\"") + "\"";
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Services/HtmlReportWriter.cs b/src/BizTalkPlatformManagementTool/Services/HtmlReportWriter.cs
new file mode 100644
index 0000000..89bb520
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Services/HtmlReportWriter.cs
@@ -0,0 +1,100 @@
+using System.Net;
+using System.Text;
+using BizTalkPlatformManagementTool.Models;
+
+namespace BizTalkPlatformManagementTool.Services
+{
+ public static class HtmlReportWriter
+ {
+ public static void WriteSnapshot(string path, BizTalkSnapshot snapshot)
+ {
+ var html = new StringBuilder();
+ Header(html, "BizTalk Snapshot");
+ html.Append("Server: ").Append(Encode(snapshot.Server)).Append(" Created: ").Append(Encode(snapshot.CreatedAt)).Append("
");
+
+ foreach (var app in snapshot.Applications)
+ {
+ html.Append("").Append(Encode(app.Application)).Append("
");
+ html.Append("Receive Locations
| Name | Status | Adapter | Address |
");
+ foreach (var item in app.ReceiveLocations)
+ {
+ StatusRow(html, item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled), item.Enabled, item.AdapterName, item.Address);
+ }
+ html.Append("
");
+
+ html.Append("Send Ports
| Name | Status | Transport | Address |
");
+ foreach (var item in app.SendPorts)
+ {
+ StatusRow(html, item.Name, ArtifactStates.FormatSendPort(item.Status), item.Status == ArtifactStates.SendPortStarted, item.PrimaryTransportType, item.PrimaryTransportAddress);
+ }
+ html.Append("
");
+
+ html.Append("Orchestrations
| Name | Status |
");
+ foreach (var item in app.Orchestrations)
+ {
+ html.Append("| ").Append(Encode(item.Name)).Append(" | ")
+ .Append(Encode(ArtifactStates.FormatOrchestration(item.OrchestrationStatus))).Append(" |
");
+ }
+ html.Append("
");
+ }
+
+ html.Append("Host Instances
| Instance | Host | Server | Status |
");
+ foreach (var item in snapshot.HostInstances)
+ {
+ html.Append("| ").Append(Encode(item.InstanceName)).Append(" | ").Append(Encode(item.HostName)).Append(" | ")
+ .Append(Encode(item.Server)).Append(" | ")
+ .Append(Encode(item.StateText)).Append(" |
");
+ }
+ html.Append("
");
+ Footer(html);
+ System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
+ }
+
+ public static void WriteDiff(string path, SnapshotDiff diff)
+ {
+ var html = new StringBuilder();
+ Header(html, "BizTalk Differences");
+
+ html.Append("Artifacts
| Application | Type | Name | Before | After |
");
+ foreach (var item in diff.ArtifactDifferences)
+ {
+ html.Append("| ").Append(Encode(item.Application)).Append(" | ").Append(Encode(item.ArtifactType)).Append(" | ")
+ .Append(Encode(item.Name)).Append(" | ").Append(Encode(item.Before)).Append(" | ").Append(Encode(item.After)).Append(" |
");
+ }
+ html.Append("
");
+
+ html.Append("Host Instances
| Instance | Host | Server | Before | After |
");
+ foreach (var item in diff.HostInstanceDifferences)
+ {
+ html.Append("| ").Append(Encode(item.InstanceName)).Append(" | ").Append(Encode(item.HostName)).Append(" | ")
+ .Append(Encode(item.Server)).Append(" | ").Append(Encode(item.Before)).Append(" | ").Append(Encode(item.After)).Append(" |
");
+ }
+ html.Append("
");
+ Footer(html);
+ System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
+ }
+
+ private static void StatusRow(StringBuilder html, string name, string status, bool ok, string detail1, string detail2)
+ {
+ html.Append("| ").Append(Encode(name)).Append(" | ").Append(Encode(status))
+ .Append(" | ").Append(Encode(detail1)).Append(" | ").Append(Encode(detail2)).Append(" |
");
+ }
+
+ private static void Header(StringBuilder html, string title)
+ {
+ html.Append("").Append(Encode(title)).Append("").Append(Encode(title)).Append("
");
+ }
+
+ private static void Footer(StringBuilder html)
+ {
+ html.Append("");
+ }
+
+ private static string Encode(string value)
+ {
+ return WebUtility.HtmlEncode(value ?? string.Empty);
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs b/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs
new file mode 100644
index 0000000..0c1e93a
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs
@@ -0,0 +1,38 @@
+using System.IO;
+using System.Runtime.Serialization.Json;
+using System.Text;
+
+namespace BizTalkPlatformManagementTool.Services
+{
+ public static class JsonFileStore
+ {
+ public static void Save(string path, T value)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
+ var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
+ {
+ UseSimpleDictionaryFormat = true
+ });
+
+ using (var stream = new MemoryStream())
+ {
+ serializer.WriteObject(stream, value);
+ var json = Encoding.UTF8.GetString(stream.ToArray());
+ File.WriteAllText(path, json, Encoding.UTF8);
+ }
+ }
+
+ public static T Load(string path)
+ {
+ var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
+ {
+ UseSimpleDictionaryFormat = true
+ });
+
+ using (var stream = File.OpenRead(path))
+ {
+ return (T)serializer.ReadObject(stream);
+ }
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs b/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs
new file mode 100644
index 0000000..3dd2ecb
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs
@@ -0,0 +1,64 @@
+using System;
+
+namespace BizTalkPlatformManagementTool.Services
+{
+ public enum LogLevel
+ {
+ Info,
+ Warning,
+ Error,
+ Success
+ }
+
+ public sealed class LogEntry
+ {
+ public DateTime Timestamp { get; set; }
+ public LogLevel Level { get; set; }
+ public string Message { get; set; }
+ }
+
+ public sealed class OperationLogger
+ {
+ private readonly Action _sink;
+
+ public OperationLogger(Action sink)
+ {
+ _sink = sink;
+ }
+
+ public void Info(string message)
+ {
+ Write(LogLevel.Info, message);
+ }
+
+ public void Warning(string message)
+ {
+ Write(LogLevel.Warning, message);
+ }
+
+ public void Error(string message)
+ {
+ Write(LogLevel.Error, message);
+ }
+
+ public void Success(string message)
+ {
+ Write(LogLevel.Success, message);
+ }
+
+ private void Write(LogLevel level, string message)
+ {
+ if (_sink == null)
+ {
+ return;
+ }
+
+ _sink(new LogEntry
+ {
+ Timestamp = DateTime.Now,
+ Level = level,
+ Message = message
+ });
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs b/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs
new file mode 100644
index 0000000..9ee419d
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs
@@ -0,0 +1,180 @@
+using System;
+using System.Collections.Generic;
+using BizTalkPlatformManagementTool.Models;
+
+namespace BizTalkPlatformManagementTool.Services
+{
+ public static class SnapshotComparer
+ {
+ public static SnapshotDiff Compare(BizTalkSnapshot before, BizTalkSnapshot after)
+ {
+ var diff = new SnapshotDiff();
+
+ CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after));
+ CompareSendPorts(diff, FlattenSendPorts(before), FlattenSendPorts(after));
+ CompareOrchestrations(diff, FlattenOrchestrations(before), FlattenOrchestrations(after));
+ CompareHostInstances(diff, before.HostInstances, after.HostInstances);
+
+ return diff;
+ }
+
+ private static void CompareReceiveLocations(SnapshotDiff diff, Dictionary before, Dictionary after)
+ {
+ foreach (var pair in after)
+ {
+ ReceiveLocationState old;
+ if (!before.TryGetValue(pair.Key, out old))
+ {
+ AddArtifact(diff, pair.Value.Application, "ReceiveLocation", pair.Value.Name, "", ArtifactStates.FormatReceiveLocation(pair.Value.Enabled));
+ }
+ else if (old.Enabled != pair.Value.Enabled)
+ {
+ AddArtifact(diff, pair.Value.Application, "ReceiveLocation", pair.Value.Name, ArtifactStates.FormatReceiveLocation(old.Enabled), ArtifactStates.FormatReceiveLocation(pair.Value.Enabled));
+ }
+ }
+
+ foreach (var pair in before)
+ {
+ if (!after.ContainsKey(pair.Key))
+ {
+ AddArtifact(diff, pair.Value.Application, "ReceiveLocation", pair.Value.Name, ArtifactStates.FormatReceiveLocation(pair.Value.Enabled), "");
+ }
+ }
+ }
+
+ private static void CompareSendPorts(SnapshotDiff diff, Dictionary before, Dictionary after)
+ {
+ foreach (var pair in after)
+ {
+ SendPortState old;
+ if (!before.TryGetValue(pair.Key, out old))
+ {
+ AddArtifact(diff, pair.Value.Application, "SendPort", pair.Value.Name, "", ArtifactStates.FormatSendPort(pair.Value.Status));
+ }
+ else if (old.Status != pair.Value.Status)
+ {
+ AddArtifact(diff, pair.Value.Application, "SendPort", pair.Value.Name, ArtifactStates.FormatSendPort(old.Status), ArtifactStates.FormatSendPort(pair.Value.Status));
+ }
+ }
+
+ foreach (var pair in before)
+ {
+ if (!after.ContainsKey(pair.Key))
+ {
+ AddArtifact(diff, pair.Value.Application, "SendPort", pair.Value.Name, ArtifactStates.FormatSendPort(pair.Value.Status), "");
+ }
+ }
+ }
+
+ private static void CompareOrchestrations(SnapshotDiff diff, Dictionary before, Dictionary after)
+ {
+ foreach (var pair in after)
+ {
+ OrchestrationState old;
+ if (!before.TryGetValue(pair.Key, out old))
+ {
+ AddArtifact(diff, pair.Value.Application, "Orchestration", pair.Value.Name, "", ArtifactStates.FormatOrchestration(pair.Value.OrchestrationStatus));
+ }
+ else if (old.OrchestrationStatus != pair.Value.OrchestrationStatus)
+ {
+ AddArtifact(diff, pair.Value.Application, "Orchestration", pair.Value.Name, ArtifactStates.FormatOrchestration(old.OrchestrationStatus), ArtifactStates.FormatOrchestration(pair.Value.OrchestrationStatus));
+ }
+ }
+
+ foreach (var pair in before)
+ {
+ if (!after.ContainsKey(pair.Key))
+ {
+ AddArtifact(diff, pair.Value.Application, "Orchestration", pair.Value.Name, ArtifactStates.FormatOrchestration(pair.Value.OrchestrationStatus), "");
+ }
+ }
+ }
+
+ private static void CompareHostInstances(SnapshotDiff diff, List before, List after)
+ {
+ var beforeMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var afterMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var item in before)
+ {
+ beforeMap[item.InstanceName ?? string.Empty] = item;
+ }
+ foreach (var item in after)
+ {
+ afterMap[item.InstanceName ?? string.Empty] = item;
+ }
+
+ foreach (var pair in afterMap)
+ {
+ HostInstanceState old;
+ if (!beforeMap.TryGetValue(pair.Key, out old))
+ {
+ diff.HostInstanceDifferences.Add(new HostInstanceDiffEntry { InstanceName = pair.Value.InstanceName, HostName = pair.Value.HostName, Server = pair.Value.Server, Before = "", After = pair.Value.StateText });
+ }
+ else if (old.RawState != pair.Value.RawState)
+ {
+ diff.HostInstanceDifferences.Add(new HostInstanceDiffEntry { InstanceName = pair.Value.InstanceName, HostName = pair.Value.HostName, Server = pair.Value.Server, Before = old.StateText, After = pair.Value.StateText });
+ }
+ }
+
+ foreach (var pair in beforeMap)
+ {
+ if (!afterMap.ContainsKey(pair.Key))
+ {
+ diff.HostInstanceDifferences.Add(new HostInstanceDiffEntry { InstanceName = pair.Value.InstanceName, HostName = pair.Value.HostName, Server = pair.Value.Server, Before = pair.Value.StateText, After = "" });
+ }
+ }
+ }
+
+ private static void AddArtifact(SnapshotDiff diff, string application, string type, string name, string before, string after)
+ {
+ diff.ArtifactDifferences.Add(new ArtifactDiffEntry
+ {
+ Application = application,
+ ArtifactType = type,
+ Name = name,
+ Before = before,
+ After = after
+ });
+ }
+
+ private static Dictionary FlattenReceiveLocations(BizTalkSnapshot snapshot)
+ {
+ var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var app in snapshot.Applications)
+ {
+ foreach (var item in app.ReceiveLocations)
+ {
+ map[item.Name ?? string.Empty] = item;
+ }
+ }
+ return map;
+ }
+
+ private static Dictionary FlattenSendPorts(BizTalkSnapshot snapshot)
+ {
+ var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var app in snapshot.Applications)
+ {
+ foreach (var item in app.SendPorts)
+ {
+ map[item.Name ?? string.Empty] = item;
+ }
+ }
+ return map;
+ }
+
+ private static Dictionary FlattenOrchestrations(BizTalkSnapshot snapshot)
+ {
+ var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var app in snapshot.Applications)
+ {
+ foreach (var item in app.Orchestrations)
+ {
+ map[item.Name ?? string.Empty] = item;
+ }
+ }
+ return map;
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Services/SnapshotStore.cs b/src/BizTalkPlatformManagementTool/Services/SnapshotStore.cs
new file mode 100644
index 0000000..817fc88
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Services/SnapshotStore.cs
@@ -0,0 +1,25 @@
+using System.IO;
+using BizTalkPlatformManagementTool.Models;
+
+namespace BizTalkPlatformManagementTool.Services
+{
+ public static class SnapshotStore
+ {
+ public static void SaveSnapshotSet(string jsonPath, BizTalkSnapshot snapshot)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
+ JsonFileStore.Save(jsonPath, snapshot);
+ CsvWriter.WriteSnapshotArtifacts(jsonPath + ".csv", snapshot);
+ CsvWriter.WriteSnapshotHosts(jsonPath + ".hosts.csv", snapshot);
+ HtmlReportWriter.WriteSnapshot(jsonPath + ".html", snapshot);
+ }
+
+ public static void SaveDiffSet(string jsonPath, SnapshotDiff diff)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
+ JsonFileStore.Save(jsonPath, diff);
+ CsvWriter.WriteDiff(Path.ChangeExtension(jsonPath, ".csv"), diff);
+ HtmlReportWriter.WriteDiff(Path.ChangeExtension(jsonPath, ".html"), diff);
+ }
+ }
+}
diff --git a/src/BizTalkPlatformManagementTool/Ui/MainForm.cs b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs
new file mode 100644
index 0000000..8428cd2
--- /dev/null
+++ b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs
@@ -0,0 +1,462 @@
+using System;
+using System.Drawing;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using BizTalkPlatformManagementTool.Models;
+using BizTalkPlatformManagementTool.Services;
+
+namespace BizTalkPlatformManagementTool.Ui
+{
+ public sealed class MainForm : Form
+ {
+ private readonly TextBox _serverTextBox = new TextBox();
+ private readonly TextBox _outputTextBox = new TextBox();
+ private readonly TextBox _stateFileTextBox = new TextBox();
+ private readonly NumericUpDown _timeoutInput = new NumericUpDown();
+ private readonly NumericUpDown _pollInput = new NumericUpDown();
+ private readonly CheckBox _dryRunCheckBox = new CheckBox();
+ private readonly DataGridView _statusGrid = new DataGridView();
+ private readonly DataGridView _logGrid = new DataGridView();
+ private readonly StatusStrip _statusStrip = new StatusStrip();
+ private readonly ToolStripStatusLabel _statusLabel = new ToolStripStatusLabel();
+ private readonly BizTalkOperationService _service;
+
+ private Button _diagnoseButton;
+ private Button _beforeButton;
+ private Button _afterButton;
+ private Button _compareButton;
+ private Button _shutdownButton;
+ private Button _restoreButton;
+
+ public MainForm()
+ {
+ Text = "BizTalk Platform Management Tool";
+ Width = 1180;
+ Height = 760;
+ MinimumSize = new Size(980, 640);
+ StartPosition = FormStartPosition.CenterScreen;
+
+ _service = new BizTalkOperationService(new OperationLogger(AppendLog));
+ BuildUi();
+ }
+
+ private void BuildUi()
+ {
+ var root = new TableLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ ColumnCount = 1,
+ RowCount = 4,
+ Padding = new Padding(10)
+ };
+ root.RowStyles.Add(new RowStyle(SizeType.Absolute, 88));
+ root.RowStyles.Add(new RowStyle(SizeType.Absolute, 54));
+ root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
+ root.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
+ Controls.Add(root);
+
+ root.Controls.Add(BuildSettingsPanel(), 0, 0);
+ root.Controls.Add(BuildActionPanel(), 0, 1);
+ root.Controls.Add(BuildTabs(), 0, 2);
+
+ _statusStrip.Items.Add(_statusLabel);
+ _statusLabel.Text = "Ready. Dry run is enabled.";
+ root.Controls.Add(_statusStrip, 0, 3);
+ }
+
+ private Control BuildSettingsPanel()
+ {
+ var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 8, RowCount = 2 };
+ panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 70));
+ panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30));
+ panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 76));
+ panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 35));
+ panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 88));
+ panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 82));
+ panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 72));
+ panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 88));
+ panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
+ panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
+
+ _serverTextBox.Text = Environment.MachineName;
+ _serverTextBox.Dock = DockStyle.Fill;
+ _outputTextBox.Text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "BizTalkPlatformManagementTool");
+ _outputTextBox.Dock = DockStyle.Fill;
+ _stateFileTextBox.Text = "before.json";
+ _stateFileTextBox.Dock = DockStyle.Fill;
+
+ _timeoutInput.Minimum = 30;
+ _timeoutInput.Maximum = 3600;
+ _timeoutInput.Value = 300;
+ _timeoutInput.Dock = DockStyle.Fill;
+
+ _pollInput.Minimum = 1;
+ _pollInput.Maximum = 60;
+ _pollInput.Value = 5;
+ _pollInput.Dock = DockStyle.Fill;
+
+ _dryRunCheckBox.Text = "Dry run";
+ _dryRunCheckBox.Checked = true;
+ _dryRunCheckBox.Dock = DockStyle.Fill;
+
+ var browseButton = new Button { Text = "Browse...", Dock = DockStyle.Fill };
+ browseButton.Click += BrowseButtonClick;
+
+ panel.Controls.Add(Label("Server"), 0, 0);
+ panel.Controls.Add(_serverTextBox, 1, 0);
+ panel.Controls.Add(Label("Output"), 2, 0);
+ panel.Controls.Add(_outputTextBox, 3, 0);
+ panel.Controls.Add(browseButton, 4, 0);
+ panel.Controls.Add(Label("Timeout"), 5, 0);
+ panel.Controls.Add(_timeoutInput, 6, 0);
+
+ panel.Controls.Add(Label("State"), 0, 1);
+ panel.Controls.Add(_stateFileTextBox, 1, 1);
+ panel.SetColumnSpan(_stateFileTextBox, 3);
+ panel.Controls.Add(_dryRunCheckBox, 4, 1);
+ panel.Controls.Add(Label("Poll sec."), 5, 1);
+ panel.Controls.Add(_pollInput, 6, 1);
+
+ return panel;
+ }
+
+ private Control BuildActionPanel()
+ {
+ var panel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, Padding = new Padding(0, 8, 0, 0) };
+ _diagnoseButton = ActionButton("Diagnose", DiagnoseClick);
+ _beforeButton = ActionButton("Snapshot Before", BeforeClick);
+ _afterButton = ActionButton("Snapshot After", AfterClick);
+ _compareButton = ActionButton("Compare", CompareClick);
+ _shutdownButton = ActionButton("Shutdown", ShutdownClick);
+ _restoreButton = ActionButton("Restore", RestoreClick);
+
+ panel.Controls.Add(_diagnoseButton);
+ panel.Controls.Add(_beforeButton);
+ panel.Controls.Add(_afterButton);
+ panel.Controls.Add(_compareButton);
+ panel.Controls.Add(_shutdownButton);
+ panel.Controls.Add(_restoreButton);
+ return panel;
+ }
+
+ private Control BuildTabs()
+ {
+ var tabs = new TabControl { Dock = DockStyle.Fill };
+ var statusPage = new TabPage("Status / Results");
+ var logPage = new TabPage("Operation Log");
+
+ ConfigureGrid(_statusGrid);
+ _statusGrid.Columns.Add("Scope", "Scope");
+ _statusGrid.Columns.Add("Application", "Application");
+ _statusGrid.Columns.Add("Name", "Name");
+ _statusGrid.Columns.Add("Status", "Status");
+ _statusGrid.Columns.Add("Details", "Details");
+ statusPage.Controls.Add(_statusGrid);
+
+ ConfigureGrid(_logGrid);
+ _logGrid.Columns.Add("Time", "Time");
+ _logGrid.Columns.Add("Level", "Level");
+ _logGrid.Columns.Add("Message", "Message");
+ _logGrid.Columns[2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
+ logPage.Controls.Add(_logGrid);
+
+ tabs.TabPages.Add(statusPage);
+ tabs.TabPages.Add(logPage);
+ return tabs;
+ }
+
+ private void DiagnoseClick(object sender, EventArgs e)
+ {
+ var server = _serverTextBox.Text.Trim();
+ RunAsync("Diagnosing WMI access...", () => _service.Diagnose(server));
+ }
+
+ private void BeforeClick(object sender, EventArgs e)
+ {
+ var options = GetOptions();
+ RunAsync("Creating before snapshot...", () =>
+ {
+ var snapshot = _service.CreateSnapshot(options.Server);
+ _service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot);
+ ShowSnapshot(snapshot);
+ });
+ }
+
+ private void AfterClick(object sender, EventArgs e)
+ {
+ var options = GetOptions();
+ RunAsync("Creating after snapshot...", () =>
+ {
+ var snapshot = _service.CreateSnapshot(options.Server);
+ _service.SaveSnapshot(options.OutputDirectory, "after.json", snapshot);
+ ShowSnapshot(snapshot);
+ });
+ }
+
+ private void CompareClick(object sender, EventArgs e)
+ {
+ var options = GetOptions();
+ RunAsync("Comparing before and after snapshots...", () =>
+ {
+ var before = JsonFileStore.Load(Path.Combine(options.OutputDirectory, "before.json"));
+ var after = JsonFileStore.Load(Path.Combine(options.OutputDirectory, "after.json"));
+ var diff = SnapshotComparer.Compare(before, after);
+ _service.SaveDiff(options.OutputDirectory, "diff.json", diff);
+ ShowDiff(diff);
+ });
+ }
+
+ private void ShutdownClick(object sender, EventArgs e)
+ {
+ if (!ConfirmDangerousAction("Shutdown"))
+ {
+ return;
+ }
+
+ var options = GetOptions();
+ RunAsync("Preparing shutdown...", () =>
+ {
+ var snapshot = _service.CreateSnapshot(options.Server);
+ _service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot);
+ var plan = _service.CreateShutdownPlan(snapshot, options.Server);
+ _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan);
+ ShowPlan(plan);
+ _service.ExecutePlan(plan, options);
+ if (!options.DryRun)
+ {
+ var after = _service.CreateSnapshot(options.Server);
+ _service.SaveSnapshot(options.OutputDirectory, "shutdown-after.json", after);
+ ShowSnapshot(after);
+ }
+ });
+ }
+
+ private void RestoreClick(object sender, EventArgs e)
+ {
+ if (!ConfirmDangerousAction("Restore"))
+ {
+ return;
+ }
+
+ var options = GetOptions();
+ RunAsync("Preparing restore...", () =>
+ {
+ var snapshot = JsonFileStore.Load(ResolveStateFile(options));
+ var plan = _service.CreateRestorePlan(snapshot, options.Server);
+ _service.SavePlan(options.OutputDirectory, "restore-plan.json", plan);
+ ShowPlan(plan);
+ _service.ExecutePlan(plan, options);
+ if (!options.DryRun)
+ {
+ var after = _service.CreateSnapshot(options.Server);
+ _service.SaveSnapshot(options.OutputDirectory, "restore-after.json", after);
+ ShowSnapshot(after);
+ }
+ });
+ }
+
+ private void RunAsync(string status, Action work)
+ {
+ SetBusy(true, status);
+ Task.Run(() =>
+ {
+ try
+ {
+ work();
+ AppendLog(new LogEntry { Timestamp = DateTime.Now, Level = LogLevel.Success, Message = "Operation completed." });
+ SetBusy(false, "Ready.");
+ }
+ catch (Exception ex)
+ {
+ AppendLog(new LogEntry { Timestamp = DateTime.Now, Level = LogLevel.Error, Message = ex.Message });
+ SetBusy(false, "Failed: " + ex.Message);
+ }
+ });
+ }
+
+ private OperationOptions GetOptions()
+ {
+ var server = string.IsNullOrWhiteSpace(_serverTextBox.Text) ? Environment.MachineName : _serverTextBox.Text.Trim();
+ var output = string.IsNullOrWhiteSpace(_outputTextBox.Text)
+ ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "BizTalkPlatformManagementTool")
+ : _outputTextBox.Text.Trim();
+ var stateFile = string.IsNullOrWhiteSpace(_stateFileTextBox.Text) ? "before.json" : _stateFileTextBox.Text.Trim();
+
+ return new OperationOptions
+ {
+ Server = server,
+ OutputDirectory = output,
+ StateFile = stateFile,
+ DryRun = _dryRunCheckBox.Checked,
+ WaitTimeoutSeconds = (int)_timeoutInput.Value,
+ PollIntervalSeconds = (int)_pollInput.Value
+ };
+ }
+
+ private string ResolveStateFile(OperationOptions options)
+ {
+ return Path.IsPathRooted(options.StateFile) ? options.StateFile : Path.Combine(options.OutputDirectory, options.StateFile);
+ }
+
+ private bool ConfirmDangerousAction(string actionName)
+ {
+ if (_dryRunCheckBox.Checked)
+ {
+ return true;
+ }
+
+ var result = MessageBox.Show(
+ actionName + " will change the BizTalk runtime state on server '" + _serverTextBox.Text + "'. Continue?",
+ "Confirm BizTalk Runtime Change",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Warning,
+ MessageBoxDefaultButton.Button2);
+ return result == DialogResult.Yes;
+ }
+
+ private void ShowSnapshot(BizTalkSnapshot snapshot)
+ {
+ InvokeIfRequired(() =>
+ {
+ _statusGrid.Rows.Clear();
+ foreach (var app in snapshot.Applications)
+ {
+ foreach (var item in app.ReceiveLocations)
+ {
+ _statusGrid.Rows.Add("ReceiveLocation", app.Application, item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled), item.AdapterName + " " + item.Address);
+ }
+ foreach (var item in app.SendPorts)
+ {
+ _statusGrid.Rows.Add("SendPort", app.Application, item.Name, ArtifactStates.FormatSendPort(item.Status), item.PrimaryTransportType + " " + item.PrimaryTransportAddress);
+ }
+ foreach (var item in app.Orchestrations)
+ {
+ _statusGrid.Rows.Add("Orchestration", app.Application, item.Name, ArtifactStates.FormatOrchestration(item.OrchestrationStatus), string.Empty);
+ }
+ }
+ foreach (var item in snapshot.HostInstances)
+ {
+ _statusGrid.Rows.Add("HostInstance", string.Empty, item.InstanceName, item.StateText, item.HostName + " on " + item.Server);
+ }
+ });
+ }
+
+ private void ShowDiff(SnapshotDiff diff)
+ {
+ InvokeIfRequired(() =>
+ {
+ _statusGrid.Rows.Clear();
+ foreach (var item in diff.ArtifactDifferences)
+ {
+ _statusGrid.Rows.Add("Diff", item.Application, item.Name, item.Before + " -> " + item.After, item.ArtifactType);
+ }
+ foreach (var item in diff.HostInstanceDifferences)
+ {
+ _statusGrid.Rows.Add("HostDiff", string.Empty, item.InstanceName, item.Before + " -> " + item.After, item.Server);
+ }
+ });
+ }
+
+ private void ShowPlan(OperationPlan plan)
+ {
+ InvokeIfRequired(() =>
+ {
+ _statusGrid.Rows.Clear();
+ foreach (var step in plan.Steps)
+ {
+ _statusGrid.Rows.Add(step.Kind, step.Application, step.Name, step.Execute ? step.Action : "Skipped", step.Warning ?? step.MethodName);
+ }
+ });
+ }
+
+ private void AppendLog(LogEntry entry)
+ {
+ InvokeIfRequired(() =>
+ {
+ var index = _logGrid.Rows.Add(entry.Timestamp.ToString("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;
+ }
+ _logGrid.FirstDisplayedScrollingRowIndex = Math.Max(0, _logGrid.Rows.Count - 1);
+ });
+ }
+
+ private void SetBusy(bool busy, string status)
+ {
+ InvokeIfRequired(() =>
+ {
+ _diagnoseButton.Enabled = !busy;
+ _beforeButton.Enabled = !busy;
+ _afterButton.Enabled = !busy;
+ _compareButton.Enabled = !busy;
+ _shutdownButton.Enabled = !busy;
+ _restoreButton.Enabled = !busy;
+ _statusLabel.Text = status;
+ });
+ }
+
+ private void BrowseButtonClick(object sender, EventArgs e)
+ {
+ using (var dialog = new FolderBrowserDialog())
+ {
+ dialog.SelectedPath = Directory.Exists(_outputTextBox.Text) ? _outputTextBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
+ if (dialog.ShowDialog(this) == DialogResult.OK)
+ {
+ _outputTextBox.Text = dialog.SelectedPath;
+ }
+ }
+ }
+
+ private void InvokeIfRequired(Action action)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (InvokeRequired)
+ {
+ BeginInvoke(action);
+ }
+ else
+ {
+ action();
+ }
+ }
+
+ private static Label Label(string text)
+ {
+ return new Label { Text = text, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft };
+ }
+
+ private static Button ActionButton(string text, EventHandler handler)
+ {
+ var button = new Button { Text = text, Width = 148, Height = 34, Margin = new Padding(0, 0, 8, 0) };
+ button.Click += handler;
+ return button;
+ }
+
+ private static void ConfigureGrid(DataGridView grid)
+ {
+ grid.Dock = DockStyle.Fill;
+ grid.AllowUserToAddRows = false;
+ grid.AllowUserToDeleteRows = false;
+ grid.ReadOnly = true;
+ grid.RowHeadersVisible = false;
+ grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
+ grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
+ grid.BackgroundColor = SystemColors.Window;
+ }
+ }
+}
diff --git a/src/BizTalkStatusTool.ps1 b/src/BizTalkStatusTool.ps1
deleted file mode 100644
index 388aec2..0000000
--- a/src/BizTalkStatusTool.ps1
+++ /dev/null
@@ -1,676 +0,0 @@
-<#
-BizTalk Status Tool – Extended Version
-Includes:
-- Applications Snapshot (RL/SP/ORCH)
-- Host Instances Snapshot
-- JSON / CSV / HTML / Console
-- Compare (Before / After)
-- HostInstance Diff (New / Removed / Changed)
-#>
-
-# ======================================================
-# PARAMETERS
-# ======================================================
-param(
- [switch]$Before,
- [switch]$After,
- [switch]$Compare,
- [switch]$Diag,
- [string]$Server,
- [string]$MgmtDbServer,
- [string]$MgmtDbName,
- [string]$OutDir
-)
-
-if (-not $Server) { $Server = $env:COMPUTERNAME }
-if (-not $OutDir) { $OutDir = "." }
-
-$ErrorActionPreference = "Stop"
-$Global:LogFile = Join-Path (Get-Location) "BizTalkStatusTool.log"
-
-# ======================================================
-# LOG
-# ======================================================
-function Write-Log {
- param([string]$Message,[ValidateSet('INFO','WARN','ERROR','DEBUG')]$Level='INFO')
-
- $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
- $line = "[$ts][$Level] $Message"
- $fg = switch ($Level) {
- INFO {"Gray"} WARN {"Yellow"} ERROR {"Red"} DEBUG {"DarkCyan"}
- }
- Write-Host $line -ForegroundColor $fg
- Add-Content -Path $Global:LogFile -Value $line
-}
-
-# ======================================================
-# STATUS MAPPING
-# ======================================================
-function Format-Status {
- param([string]$ArtifactType, $Value)
-
- $r = [ordered]@{
- Text = ""
- Class = "neutral"
- ConsoleColor = "Yellow"
- }
-
- switch ($ArtifactType) {
-
- "ReceiveLocation" {
- if ($Value -eq $true) {
- $r.Text="Enabled"; $r.Class="ok"; $r.ConsoleColor="Green"
- }
- elseif ($Value -eq $false) {
- $r.Text="Disabled"; $r.Class="bad"; $r.ConsoleColor="Red"
- }
- }
-
- "SendPort" {
- switch ([int]$Value) {
- 3 { $r.Text="Started"; $r.Class="ok"; $r.ConsoleColor="Green" }
- 2 { $r.Text="Stopped"; $r.Class="bad"; $r.ConsoleColor="Red" }
- 1 { $r.Text="Bound"; $r.Class="bad"; $r.ConsoleColor="Red" }
- default { $r.Text="Unknown"; $r.Class="bad"; $r.ConsoleColor="Red" }
- }
- }
-
- "Orchestration" {
- switch ([int]$Value) {
- 4 { $r.Text="Started"; $r.Class="ok"; $r.ConsoleColor="Green" }
- 3 { $r.Text="Stopped"; $r.Class="bad"; $r.ConsoleColor="Red" }
- 2 { $r.Text="Bound"; $r.Class="bad"; $r.ConsoleColor="Red" }
- default { $r.Text="Unknown"; $r.Class="bad"; $r.ConsoleColor="Red" }
- }
- }
-
- "HostInstance" {
- switch ([int]$Value) {
- 4 { $r.Text="Started"; $r.Class="ok"; $r.ConsoleColor="Green" }
- 2 { $r.Text="StartPending"; $r.Class="warn"; $r.ConsoleColor="Yellow" }
- 3 { $r.Text="StopPending"; $r.Class="warn"; $r.ConsoleColor="Yellow" }
- 1 { $r.Text="Stopped"; $r.Class="bad"; $r.ConsoleColor="Red" }
- default { $r.Text="Unknown"; $r.Class="bad"; $r.ConsoleColor="Red" }
- }
- }
- }
-
- return [pscustomobject]$r
-}
-
-# ======================================================
-# MGMT DB
-# ======================================================
-function Get-BizTalkMgmtDbInfo {
- param([string]$MgmtDbServer,[string]$MgmtDbName)
-
- $reg = "HKLM:\SOFTWARE\Microsoft\BizTalk Server\3.0\Administration"
-
- try {
- $p = Get-ItemProperty $reg
- if (-not $MgmtDbServer) { $MgmtDbServer = $p.MgmtDBServer }
- if (-not $MgmtDbName) { $MgmtDbName = $p.MgmtDBName }
- }
- catch {
- if (-not $MgmtDbServer) { $MgmtDbServer = "localhost" }
- if (-not $MgmtDbName) { $MgmtDbName = "BizTalkMgmtDb" }
- }
-
- return @{ Server=$MgmtDbServer; Name=$MgmtDbName }
-}
-
-# ======================================================
-# HOST INSTANCES
-# ======================================================
-function Get-BizTalkHostInstances {
- param([string]$Server)
-
- $wmi = @{
- Namespace = "root\MicrosoftBizTalkServer"
- ComputerName = $Server
- ErrorAction = 'Stop'
- }
-
- $list = Get-WmiObject MSBTS_HostInstance @wmi
-
- $result = foreach ($hi in $list) {
- $st = Format-Status -ArtifactType "HostInstance" -Value $hi.ServiceState
- [pscustomobject]@{
- InstanceName = $hi.InstanceName
- HostName = $hi.HostName
- Server = $hi.RunningServer
- RawState = $hi.ServiceState
- StateText = $st.Text
- StateClass = $st.Class
- ConsoleColor = $st.ConsoleColor
- }
- }
-
- return $result
-}
-
-function Show-HostInstancesConsole {
- param($HostInstances)
- Write-Host "`n=== Host Instances ===" -ForegroundColor Yellow
- foreach ($hi in $HostInstances) {
- Write-Host ("{0,-35} {1,-20} {2,-10}" -f $hi.InstanceName,$hi.HostName,$hi.StateText) `
- -ForegroundColor $hi.ConsoleColor
- }
-}
-
-function Export-HostInstancesHtml {
- param($HostInstances)
-
- $html = @()
- $html += "Host Instances
"
- $html += ""
- $html += "| Instance | Host | Server | Status |
"
-
- foreach ($hi in $HostInstances) {
- $html += "
- | $($hi.InstanceName) |
- $($hi.HostName) |
- $($hi.Server) |
- $($hi.StateText) |
-
"
- }
-
- $html += "
"
- return ($html -join "`n")
-}
-
-function Export-HostInstancesCsv {
- param($HostInstances, [string]$CsvFile)
- $HostInstances |
- Select InstanceName,HostName,Server,StateText |
- Export-Csv -NoTypeInformation -Encoding UTF8 -Path $CsvFile
-}
-
-# ======================================================
-# SNAPSHOT
-# ======================================================
-function Get-BizTalkSnapshot {
- param(
- [string]$OutputFile,
- [string]$Server,
- [string]$MgmtDbServer,
- [string]$MgmtDbName
- )
-
- Write-Log "Snapshot: Server=$Server File=$OutputFile"
-
- [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.BizTalk.ExplorerOM")
- $db = Get-BizTalkMgmtDbInfo -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName
-
- $cat = New-Object Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer
- $cat.ConnectionString = "Server={0};Database={1};Integrated Security=SSPI" -f $db.Server,$db.Name
-
- $apps = $cat.Applications
-
- # WMI lookup tables
- $wmi = @{ Namespace='root\MicrosoftBizTalkServer'; ComputerName=$Server; ErrorAction='Stop' }
-
- $rlW = Get-WmiObject MSBTS_ReceiveLocation @wmi |
- Select Name, @{n='Enabled';e={-not $_.IsDisabled}}, AdapterName, @{n='Address';e={$_.InboundTransportURL}}
-
- $spW = Get-WmiObject MSBTS_SendPort @wmi |
- Select Name, @{n='Status';e={$_.Status}}, @{n='PrimaryTransportType';e={$_.PTTransportType}},
- @{n='PrimaryTransportAddress';e={$_.PTAddress}}
-
- $orW = Get-WmiObject MSBTS_Orchestration @wmi |
- Select Name, @{n='OrchestrationStatus';e={$_.OrchestrationStatus}}
-
- $mapRL=@{}; foreach($i in $rlW){$mapRL[$i.Name]=$i}
- $mapSP=@{}; foreach($i in $spW){$mapSP[$i.Name]=$i}
- $mapOR=@{}; foreach($i in $orW){$mapOR[$i.Name]=$i}
-
- $snapshot=@()
-
- foreach ($app in $apps) {
-
- $rlItems=@()
- foreach ($rp in $app.ReceivePorts) {
- foreach ($rl in $rp.ReceiveLocations) {
- if ($mapRL.ContainsKey($rl.Name)) {
- $w = $mapRL[$rl.Name]
- $rlItems += [pscustomobject]@{
- Name=$rl.Name
- Enabled=$w.Enabled
- AdapterName=$w.AdapterName
- Address=$w.Address
- }
- }
- }
- }
-
- $spItems=@()
- foreach ($sp in $app.SendPorts) {
- if ($mapSP.ContainsKey($sp.Name)) {
- $w = $mapSP[$sp.Name]
- $spItems += [pscustomobject]@{
- Name=$sp.Name
- Status=$w.Status
- PrimaryTransportType=$w.PrimaryTransportType
- PrimaryTransportAddress=$w.PrimaryTransportAddress
- }
- }
- }
-
- $orItems=@()
- foreach ($o in $app.Orchestrations) {
- $key = if ($mapOR.ContainsKey($o.FullName)) { $o.FullName } else { $o.Name }
- if ($mapOR.ContainsKey($key)) {
- $w = $mapOR[$key]
- $orItems += [pscustomobject]@{
- Name=$o.FullName
- OrchestrationStatus=$w.OrchestrationStatus
- }
- }
- }
-
- $snapshot += [pscustomobject]@{
- Application=$app.Name
- ReceiveLocations=$rlItems
- SendPorts=$spItems
- Orchestrations=$orItems
- }
- }
-
- # Host Instances
- $hostInstances = Get-BizTalkHostInstances -Server $Server
-
- # Export JSON (Applications + HostInstances)
- $full = [pscustomobject]@{
- Applications = $snapshot
- HostInstances = $hostInstances
- }
-
- $full | ConvertTo-Json -Depth 8 | Out-File $OutputFile -Encoding UTF8
-
- # Export CSV
- $csvArtifact = foreach ($app in $snapshot) {
- foreach ($rl in $app.ReceiveLocations) {
- $st = Format-Status -ArtifactType "ReceiveLocation" -Value $rl.Enabled
- [pscustomobject]@{
- Application=$app.Application
- Type="ReceiveLocation"
- Name=$rl.Name
- Status=$st.Text
- }
- }
- foreach ($sp in $app.SendPorts) {
- $st = Format-Status -ArtifactType "SendPort" -Value $sp.Status
- [pscustomobject]@{
- Application=$app.Application
- Type="SendPort"
- Name=$sp.Name
- Status=$st.Text
- }
- }
- foreach ($o in $app.Orchestrations) {
- $st = Format-Status -ArtifactType "Orchestration" -Value $o.OrchestrationStatus
- [pscustomobject]@{
- Application=$app.Application
- Type="Orchestration"
- Name=$o.Name
- Status=$st.Text
- }
- }
- }
-
- $csvArtifact |
- Export-Csv -Encoding UTF8 -NoTypeInformation -Path ($OutputFile + ".csv")
-
- Export-HostInstancesCsv -HostInstances $hostInstances -CsvFile ($OutputFile + ".hosts.csv")
-
- # HTML
- $html=@()
- $html+=""
- $html+="BizTalk Snapshot
"
-
- foreach ($app in $snapshot) {
- $html+="Application: $($app.Application)
"
-
- if ($app.ReceiveLocations.Count -gt 0) {
- $html+="Receive Locations
| Name | Status |
"
- foreach ($rl in $app.ReceiveLocations) {
- $st = Format-Status -ArtifactType "ReceiveLocation" -Value $rl.Enabled
- $html+="| $($rl.Name) | $($st.Text) |
"
- }
- $html+="
"
- }
-
- if ($app.SendPorts.Count -gt 0) {
- $html+="Send Ports
| Name | Status |
"
- foreach ($sp in $app.SendPorts) {
- $st = Format-Status -ArtifactType "SendPort" -Value $sp.Status
- $html+="| $($sp.Name) | $($st.Text) |
"
- }
- $html+="
"
- }
-
- if ($app.Orchestrations.Count -gt 0) {
- $html+="Orchestrations
| Name | Status |
"
- foreach ($o in $app.Orchestrations) {
- $st = Format-Status -ArtifactType "Orchestration" -Value $o.OrchestrationStatus
- $html+="| $($o.Name) | $($st.Text) |
"
- }
- $html+="
"
- }
- }
-
- # Add Host Instances
- $html += Export-HostInstancesHtml -HostInstances $hostInstances
-
- $html+=""
- $html -join "`n" | Out-File ($OutputFile + ".html") -Encoding UTF8
-
- # Console Output
- Write-Host "`n===== Snapshot =====" -ForegroundColor Yellow
- foreach ($app in $snapshot) {
- Write-Host "`nApplication: $($app.Application)" -ForegroundColor Cyan
-
- foreach ($rl in $app.ReceiveLocations) {
- $st = Format-Status -ArtifactType "ReceiveLocation" -Value $rl.Enabled
- Write-Host (" RL {0,-60} {1}" -f $rl.Name,$st.Text) -ForegroundColor $st.ConsoleColor
- }
-
- foreach ($sp in $app.SendPorts) {
- $st = Format-Status -ArtifactType "SendPort" -Value $sp.Status
- Write-Host (" SP {0,-60} {1}" -f $sp.Name,$st.Text) -ForegroundColor $st.ConsoleColor
- }
-
- foreach ($o in $app.Orchestrations) {
- $st = Format-Status -ArtifactType "Orchestration" -Value $o.OrchestrationStatus
- Write-Host (" OR {0,-60} {1}" -f $o.Name,$st.Text) -ForegroundColor $st.ConsoleColor
- }
- }
-
- Show-HostInstancesConsole -HostInstances $hostInstances
-
- Write-Host "Snapshot erstellt: $(Resolve-Path $OutputFile)" -ForegroundColor Green
-}
-
-# ======================================================
-# COMPARE (Before/After)
-# ======================================================
-function Compare-BizTalkSnapshots {
- param(
- [string]$Before,
- [string]$After,
- [string]$HtmlReport = "BizTalkDiff.html"
- )
-
- $b = Get-Content $Before | ConvertFrom-Json
- $a = Get-Content $After | ConvertFrom-Json
-
- $diff = @()
-
- function AddDiff {
- param($app,$type,$name,$before,$after)
-
- $bf = Format-Status -ArtifactType $type -Value $before
- $af = Format-Status -ArtifactType $type -Value $after
-
- $script:diff += [pscustomobject]@{
- Application = $app
- ArtifactType= $type
- Name = $name
- BeforeText = $bf.Text
- BeforeClass = $bf.Class
- AfterText = $af.Text
- AfterClass = $af.Class
- }
- }
-
- foreach ($appAfter in $a.Applications) {
- $appBefore = $b.Applications | Where-Object {$_.Application -eq $appAfter.Application}
-
- # RECEIVE LOCATIONS
- foreach ($x in $appAfter.ReceiveLocations) {
- $old = $appBefore.ReceiveLocations | Where-Object {$_.Name -eq $x.Name}
- if (-not $old) { AddDiff $appAfter.Application "ReceiveLocation" $x.Name $null $x.Enabled; continue }
- if ($old.Enabled -ne $x.Enabled) { AddDiff $appAfter.Application "ReceiveLocation" $x.Name $old.Enabled $x.Enabled }
- }
- foreach ($x in $appBefore.ReceiveLocations) {
- if (-not ($appAfter.ReceiveLocations | Where-Object {$_.Name -eq $x.Name})) {
- AddDiff $appBefore.Application "ReceiveLocation" $x.Name $x.Enabled $null
- }
- }
-
- # SEND PORTS
- foreach ($x in $appAfter.SendPorts) {
- $old = $appBefore.SendPorts | Where-Object {$_.Name -eq $x.Name}
- if (-not $old) { AddDiff $appAfter.Application "SendPort" $x.Name $null $x.Status; continue }
- if ($old.Status -ne $x.Status) { AddDiff $appAfter.Application "SendPort" $x.Name $old.Status $x.Status }
- }
- foreach ($x in $appBefore.SendPorts) {
- if (-not ($appAfter.SendPorts | Where-Object {$_.Name -eq $x.Name})) {
- AddDiff $appBefore.Application "SendPort" $x.Name $x.Status $null
- }
- }
-
- # ORCHESTRATIONS
- foreach ($x in $appAfter.Orchestrations) {
- $old = $appBefore.Orchestrations | Where-Object {$_.Name -eq $x.Name}
- if (-not $old) { AddDiff $appAfter.Application "Orchestration" $x.Name $null $x.OrchestrationStatus; continue }
- if ($old.OrchestrationStatus -ne $x.OrchestrationStatus) {
- AddDiff $appAfter.Application "Orchestration" $x.Name $old.OrchestrationStatus $x.OrchestrationStatus
- }
- }
- foreach ($x in $appBefore.Orchestrations) {
- if (-not ($appAfter.Orchestrations | Where-Object {$_.Name -eq $x.Name})) {
- AddDiff $appBefore.Application "Orchestration" $x.Name $x.OrchestrationStatus $null
- }
- }
- }
-
- # ======================================================
- # HOST INSTANCE DIFF (Option H-DIFF2)
- # ======================================================
-
- $beforeHI = $b.HostInstances
- $afterHI = $a.HostInstances
-
- $hiDiff_New=@()
- $hiDiff_Removed=@()
- $hiDiff_Changed=@()
-
- # New
- foreach ($hi in $afterHI) {
- if (-not ($beforeHI | Where-Object {$_.InstanceName -eq $hi.InstanceName})) {
- $hiDiff_New += $hi
- }
- }
-
- # Removed
- foreach ($hi in $beforeHI) {
- if (-not ($afterHI | Where-Object {$_.InstanceName -eq $hi.InstanceName})) {
- $hiDiff_Removed += $hi
- }
- }
-
- # Changed
- foreach ($hiAfter in $afterHI) {
- $hiBefore = $beforeHI | Where-Object {$_.InstanceName -eq $hiAfter.InstanceName}
- if ($hiBefore) {
- if ($hiBefore.StateText -ne $hiAfter.StateText) {
- $hiDiff_Changed += [pscustomobject]@{
- Instance = $hiAfter.InstanceName
- Before = $hiBefore.StateText
- After = $hiAfter.StateText
- }
- }
- }
- }
-
- # ======================================================
- # JSON DIFF EXPORT
- # ======================================================
- $jsonDiff = [pscustomobject]@{
- ArtifactsDiff = $diff
- HostInstances = [pscustomobject]@{
- New = $hiDiff_New
- Removed = $hiDiff_Removed
- Changed = $hiDiff_Changed
- }
- }
-
- $jsonDiff | ConvertTo-Json -Depth 8 | Out-File "diff.json" -Encoding UTF8
-
- # ======================================================
- # CSV DIFF EXPORT
- # ======================================================
- $diff | Export-Csv -Path "diff.csv" -NoTypeInformation -Encoding UTF8
-
- # ======================================================
- # HTML DIFF EXPORT
- # ======================================================
- $html=@()
- $html+=""
- $html+="BizTalk Differences
"
-
- # Artifacts
- $html+="Artifact Differences
"
- $html+="| Application | Type | Name | Before | After |
"
- foreach ($d in $diff) {
- $html+="
- | $($d.Application) |
- $($d.ArtifactType) |
- $($d.Name) |
- $($d.BeforeText) |
- $($d.AfterText) |
-
"
- }
- $html+="
"
-
- # HostInstance DIFF
- $html+="Host Instance Changes
"
-
- # NEW
- $html+="New
| Instance | Host | Server | Status |
"
- foreach ($hi in $hiDiff_New) {
- $html+="
- | $($hi.InstanceName) |
- $($hi.HostName) |
- $($hi.Server) |
- $($hi.StateText) |
-
"
- }
- $html+="
"
-
- # REMOVED
- $html+="Removed
| Instance | Host | Server | Status |
"
- foreach ($hi in $hiDiff_Removed) {
- $html+="
- | $($hi.InstanceName) |
- $($hi.HostName) |
- $($hi.Server) |
- $($hi.StateText) |
-
"
- }
- $html+="
"
-
- # CHANGED
- $html+="Changed
| Instance | Before | After |
"
- foreach ($hi in $hiDiff_Changed) {
- $before = Format-Status -ArtifactType "HostInstance" -Value $hi.Before
- $after = Format-Status -ArtifactType "HostInstance" -Value $hi.After
- $html+="
- | $($hi.Instance) |
- $($hi.Before) |
- $($hi.After) |
-
"
- }
- $html+="
"
-
- $html+=""
- $html -join "`n" | Out-File $HtmlReport -Encoding UTF8
-
- # ======================================================
- # CONSOLE OUTPUT
- # ======================================================
- Write-Host "`n===== BizTalk DIFF =====" -ForegroundColor Yellow
-
- if ($diff.Count -eq 0 -and
- $hiDiff_New.Count -eq 0 -and
- $hiDiff_Removed.Count -eq 0 -and
- $hiDiff_Changed.Count -eq 0)
- {
- Write-Host "Keine Unterschiede." -ForegroundColor Green
- }
- else {
- Write-Host "`n--- Artifacts ---" -ForegroundColor Cyan
- foreach ($d in $diff) {
- Write-Host "`nApplication: $($d.Application)" -ForegroundColor Cyan
- Write-Host "Artifact: $($d.ArtifactType)"
- Write-Host "Name: $($d.Name)"
- Write-Host ("Before: {0}" -f $d.BeforeText) -ForegroundColor $(
- if ($d.BeforeClass -eq "ok") {"Green"} elseif ($d.BeforeClass -eq "warn") {"Yellow"} else {"Red"})
- Write-Host ("After: {0}" -f $d.AfterText) -ForegroundColor $(
- if ($d.AfterClass -eq "ok") {"Green"} elseif ($d.AfterClass -eq "warn") {"Yellow"} else {"Red"})
- }
-
- Write-Host "`n--- Host Instances (New) ---" -ForegroundColor Cyan
- foreach ($hi in $hiDiff_New) {
- Write-Host ("NEW: {0} {1}" -f $hi.InstanceName,$hi.StateText) -ForegroundColor $hi.ConsoleColor
- }
-
- Write-Host "`n--- Host Instances (Removed) ---" -ForegroundColor Cyan
- foreach ($hi in $hiDiff_Removed) {
- Write-Host ("REMOVED: {0} {1}" -f $hi.InstanceName,$hi.StateText) -ForegroundColor Red
- }
-
- Write-Host "`n--- Host Instances (Changed) ---" -ForegroundColor Cyan
- foreach ($hi in $hiDiff_Changed) {
- Write-Host ("CHANGED: {0} {1} -> {2}" -f $hi.Instance,$hi.Before,$hi.After) -ForegroundColor Yellow
- }
- }
-
- Write-Host "HTML Diff exportiert: $HtmlReport" -ForegroundColor Green
-}
-
-# ======================================================
-# DISPATCH
-# ======================================================
-if (-not (Test-Path $OutDir)) {
- New-Item -ItemType Directory -Path $OutDir | Out-Null
-}
-Set-Location $OutDir
-Write-Log "Working dir: $(Get-Location)"
-
-if ($Diag) {
- Write-Host "DIAG → Server: $Server" -ForegroundColor Yellow
- try {
- $sp = Get-WmiObject MSBTS_SendPort -Namespace root\MicrosoftBizTalkServer -ComputerName $Server |
- Select -First 3 Name,Status
- Write-Host "WMI OK:" -ForegroundColor Green
- $sp | Format-Table
- }
- catch {
- Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
- }
- exit
-}
-
-if ($Before) { Get-BizTalkSnapshot -OutputFile "before.json" -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName; exit }
-if ($After) { Get-BizTalkSnapshot -OutputFile "after.json" -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName; exit }
-if ($Compare){ Compare-BizTalkSnapshots -Before "before.json" -After "after.json"; exit }
-
-Write-Host "Usage:"
-Write-Host " .\BizTalkStatusTool.ps1 -Before"
-Write-Host " .\BizTalkStatusTool.ps1 -After"
-Write-Host " .\BizTalkStatusTool.ps1 -Compare"
-Write-Host " .\BizTalkStatusTool.ps1 -Diag"
\ No newline at end of file