Fixed host instances WMI query error when dealing with shutdown / restart

This commit is contained in:
2026-06-01 13:47:59 +02:00
parent 6c837f35f5
commit ae468e9f19
10 changed files with 290 additions and 54 deletions
+3
View File
@@ -13,6 +13,9 @@ obj/
*.log *.log
*.json *.json
*.html *.html
*.zip
*.txt
*.png
work/ work/
release/* release/*
+11 -3
View File
@@ -1,7 +1,7 @@
# BizTalk Platform Management Tool Dokumentation # BizTalk Platform Management Tool Dokumentation
**Stand:** 2026-04-27 **Stand:** 2026-04-27
**Implementierung:** C# WinForms, .NET Framework 4.7.1 **Implementierung:** C# WinForms, .NET Framework 4.6.1
**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1` **Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
## Zweck ## Zweck
@@ -40,10 +40,13 @@ Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster
- Dry-run ist standardmäßig aktiviert. - Dry-run ist standardmäßig aktiviert.
- Echte Shutdown-/Restore-Aktionen verlangen bei deaktiviertem Dry-run eine zusätzliche Bestätigung. - Echte Shutdown-/Restore-Aktionen verlangen bei deaktiviertem Dry-run eine zusätzliche Bestätigung.
- Jede Operation schreibt Einträge in das sichtbare Operation Log. - Jede Operation schreibt Einträge in das sichtbare Operation Log.
- Zusätzlich wird neben der EXE eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben.
- Logdateien werden rollierend für den aktuellen Tag plus vier vorherige Tage vorgehalten.
- Operationspläne werden vor Laufzeitänderungen gespeichert. - Operationspläne werden vor Laufzeitänderungen gespeichert.
- WMI-Methodenrückgaben werden geprüft. - WMI-Methodenrückgaben werden geprüft.
- Wartezeiten nutzen konfigurierbare Timeout- und Polling-Werte. - Wartezeiten nutzen konfigurierbare Timeout- und Polling-Werte.
- Host Instances auf anderen Servern werden übersprungen und als Warnung protokolliert. - Host Instances auf anderen Servern werden übersprungen und als Warnung protokolliert.
- Echte Shutdown-/Restore-Schritte protokollieren WMI-Klasse, Schlüssel, Zielobjekt und Methode, damit Fehler wie WMI-Query- oder Methodenfehler eindeutig zugeordnet werden können.
## Shutdown-Reihenfolge ## Shutdown-Reihenfolge
@@ -73,6 +76,11 @@ Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster
- Nachher-Snapshots: `shutdown-after.json`, `restore-after.json` - Nachher-Snapshots: `shutdown-after.json`, `restore-after.json`
- Diff: `diff.json`, `diff.csv`, `diff.html` - Diff: `diff.json`, `diff.csv`, `diff.html`
- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html` - Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html`
- Laufzeitlogs neben der EXE: `BizTalkPlatformManagementTool-yyyy-MM-dd.log`
## Fehleranalyse
Bei echten Shutdown- und Restore-Aktionen wird jeder Schritt vor der Ausführung mit Artefakttyp, WMI-Klasse, Schlüsselproperty, Schlüsselwert und Methodenname protokolliert. Die Objektauflösung verwendet eine breite `SELECT * FROM <class>`-Abfrage und filtert danach im Prozess auf den Schlüsselwert. Dadurch können Host-Instance-Namen und andere BizTalk-Namen mit Sonderzeichen keine ungültige WMI-WQL-`WHERE`-Query mehr erzeugen.
## Status Mapping ## Status Mapping
@@ -83,7 +91,7 @@ Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster
## Anforderungen ## Anforderungen
- Windows Server oder Administrationshost mit .NET Framework 4.7.1 - Windows Server oder Administrationshost mit .NET Framework 4.6.1
- BizTalk Server 2020 oder installierte BizTalk Administration Tools - BizTalk Server 2020 oder installierte BizTalk Administration Tools
- Berechtigungen auf den WMI-Namespace `root\MicrosoftBizTalkServer` - Berechtigungen auf den WMI-Namespace `root\MicrosoftBizTalkServer`
- Visual Studio mit .NET Framework 4.7.1 Developer Pack für Builds - Visual Studio mit .NET Framework 4.6.1 Developer Pack für Builds
+4 -2
View File
@@ -4,10 +4,11 @@
- Windows Server 2019/2022 oder ein Windows-Administrationshost - Windows Server 2019/2022 oder ein Windows-Administrationshost
- Microsoft BizTalk Server 2020 oder BizTalk Administration Tools - Microsoft BizTalk Server 2020 oder BizTalk Administration Tools
- .NET Framework 4.7.1 Runtime - .NET Framework 4.6.1 Runtime
- Für Builds: Visual Studio mit .NET Framework 4.7.1 Developer Pack - Für Builds: Visual Studio mit .NET Framework 4.6.1 Developer Pack
- Zugriff auf den WMI-Namespace `root\MicrosoftBizTalkServer` - Zugriff auf den WMI-Namespace `root\MicrosoftBizTalkServer`
- Ausreichende Rechte zum Lesen und Ändern von BizTalk-Artefakten - Ausreichende Rechte zum Lesen und Ändern von BizTalk-Artefakten
- Schreibrechte im Verzeichnis der EXE für die tägliche Logdatei
## Build ## Build
@@ -23,6 +24,7 @@
2. Sicherstellen, dass der ausführende Benutzer WMI-Zugriff auf `root\MicrosoftBizTalkServer` hat. 2. Sicherstellen, dass der ausführende Benutzer WMI-Zugriff auf `root\MicrosoftBizTalkServer` hat.
3. `BizTalkPlatformManagementTool.exe` starten. 3. `BizTalkPlatformManagementTool.exe` starten.
4. Als Ausgabeverzeichnis einen Ordner wählen, in dem Plan-, Snapshot- und Report-Dateien abgelegt werden dürfen. 4. Als Ausgabeverzeichnis einen Ordner wählen, in dem Plan-, Snapshot- und Report-Dateien abgelegt werden dürfen.
5. Prüfen, dass im EXE-Verzeichnis `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben werden kann. Logs werden für maximal fünf Tage vorgehalten.
## Erster Funktionstest ## Erster Funktionstest
+11 -3
View File
@@ -5,7 +5,7 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
## Current State ## Current State
- Application: C# WinForms - Application: C# WinForms
- Target framework: .NET Framework 4.7.1 - Target framework: .NET Framework 4.6.1
- Solution: `BizTalkPlatformManagementTool.sln` - Solution: `BizTalkPlatformManagementTool.sln`
- Project: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj` - Project: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
- Primary namespace and assembly: `BizTalkPlatformManagementTool` - Primary namespace and assembly: `BizTalkPlatformManagementTool`
@@ -20,6 +20,7 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
- Host instance handling for the selected BizTalk server - Host instance handling for the selected BizTalk server
- Dry-run mode enabled by default - Dry-run mode enabled by default
- WMI access through `root\MicrosoftBizTalkServer` - WMI access through `root\MicrosoftBizTalkServer`
- Detailed operation logging in the GUI and daily rolling log files next to the executable
- No compile-time dependency on BizTalk ExplorerOM assemblies - No compile-time dependency on BizTalk ExplorerOM assemblies
## Safe Usage ## Safe Usage
@@ -56,12 +57,19 @@ Orchestrations that were `Bound` are deliberately left unchanged during restore
- `shutdown-after.json`, `restore-after.json` - `shutdown-after.json`, `restore-after.json`
- `diff.json`, `diff.csv`, `diff.html` - `diff.json`, `diff.csv`, `diff.html`
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html` - Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
- Runtime logs next to the executable: `BizTalkPlatformManagementTool-yyyy-MM-dd.log`
Log files are retained for the current day plus the previous four days. Older `BizTalkPlatformManagementTool-*.log` files are removed on startup.
## Troubleshooting
The Operation Log shows the WMI class, key property, key value and method for real shutdown and restore steps. WMI objects are resolved with a broad `SELECT * FROM <class>` query and a client-side key filter so names containing special characters do not break the WMI query parser.
## Build ## 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. Open `BizTalkPlatformManagementTool.sln` in Visual Studio on Windows with the .NET Framework 4.6.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. The app targets .NET Framework 4.6.1 for compatibility with customer environments that do not have newer .NET Framework developer packs installed.
## Documentation ## Documentation
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<startup> <startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" /> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup> </startup>
</configuration> </configuration>
@@ -8,7 +8,7 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<RootNamespace>BizTalkPlatformManagementTool</RootNamespace> <RootNamespace>BizTalkPlatformManagementTool</RootNamespace>
<AssemblyName>BizTalkPlatformManagementTool</AssemblyName> <AssemblyName>BizTalkPlatformManagementTool</AssemblyName>
<TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion> <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
@@ -9,7 +9,7 @@ namespace BizTalkPlatformManagementTool.Services
{ {
public sealed class BizTalkOperationService public sealed class BizTalkOperationService
{ {
public const string Version = "2.0.0-net471"; public const string Version = "2.0.0-net461";
private const string UnknownApplication = "(Unknown Application)"; private const string UnknownApplication = "(Unknown Application)";
private readonly OperationLogger _logger; private readonly OperationLogger _logger;
@@ -116,7 +116,7 @@ namespace BizTalkPlatformManagementTool.Services
{ {
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled)) foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
{ {
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, false)); plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null));
} }
foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted)) foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
@@ -232,6 +232,9 @@ namespace BizTalkPlatformManagementTool.Services
continue; continue;
} }
try
{
_logger.Info("Executing step: " + DescribeStep(step));
using (var instance = client.FindByProperty(step.WmiClass, step.KeyProperty, step.KeyValue)) using (var instance = client.FindByProperty(step.WmiClass, step.KeyProperty, step.KeyValue))
{ {
if (instance == null) if (instance == null)
@@ -242,6 +245,11 @@ namespace BizTalkPlatformManagementTool.Services
ExecuteStep(client, instance, step, options); ExecuteStep(client, instance, step, options);
} }
} }
catch (Exception ex)
{
throw new InvalidOperationException("Step failed: " + DescribeStep(step) + ". Error: " + ex.Message, ex);
}
}
} }
} }
@@ -387,6 +395,23 @@ namespace BizTalkPlatformManagementTool.Services
return result; return result;
} }
private static string DescribeStep(OperationStep step)
{
if (step == null)
{
return "<unknown step>";
}
return step.Action
+ " '" + step.Name + "'"
+ " [kind=" + step.Kind
+ ", class=" + step.WmiClass
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
+ ", method=" + step.MethodName
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
+ "]";
}
private static ApplicationSnapshot GetApplication(Dictionary<string, ApplicationSnapshot> apps, string appName) private static ApplicationSnapshot GetApplication(Dictionary<string, ApplicationSnapshot> apps, string appName)
{ {
ApplicationSnapshot app; ApplicationSnapshot app;
@@ -34,11 +34,24 @@ namespace BizTalkPlatformManagementTool.Services
} }
public List<ManagementObject> Query(string className) public List<ManagementObject> Query(string className)
{
return Query(className, true);
}
private List<ManagementObject> Query(string className, bool logQuery)
{ {
EnsureConnected(); EnsureConnected();
var result = new List<ManagementObject>(); var result = new List<ManagementObject>();
var query = new ObjectQuery("SELECT * FROM " + className); var queryText = "SELECT * FROM " + className;
if (logQuery)
{
_logger.Info("Executing WMI query: " + queryText);
}
try
{
var query = new ObjectQuery(queryText);
using (var searcher = new ManagementObjectSearcher(_scope, query)) using (var searcher = new ManagementObjectSearcher(_scope, query))
using (var collection = searcher.Get()) using (var collection = searcher.Get())
{ {
@@ -47,31 +60,62 @@ namespace BizTalkPlatformManagementTool.Services
result.Add(item); result.Add(item);
} }
} }
}
catch (ManagementException ex)
{
throw new InvalidOperationException("WMI query failed. Query: " + queryText + ". WMI error: " + ex.Message, ex);
}
return result; return result;
} }
public ManagementObject FindByProperty(string className, string propertyName, string value) public ManagementObject FindByProperty(string className, string propertyName, string value)
{
return FindByProperty(className, propertyName, value, true);
}
private ManagementObject FindByProperty(string className, string propertyName, string value, bool logLookup)
{ {
EnsureConnected(); EnsureConnected();
var query = string.Format( var queryText = "SELECT * FROM " + className;
var filter = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"SELECT * FROM {0} WHERE {1} = \"{2}\"", "{0} = \"{1}\"",
className,
propertyName, propertyName,
EscapeWql(value)); value ?? string.Empty);
if (logLookup)
{
_logger.Info("Resolving WMI object: " + queryText + " with client-side filter " + filter);
}
using (var searcher = new ManagementObjectSearcher(_scope, new ObjectQuery(query))) try
using (var collection = searcher.Get())
{ {
foreach (ManagementObject item in collection) ManagementObject match = null;
var items = Query(className, false);
foreach (var item in items)
{ {
return item; if (MatchesProperty(item, className, propertyName, value))
{
match = item;
break;
} }
} }
return null; foreach (var item in items)
{
if (!object.ReferenceEquals(item, match))
{
item.Dispose();
}
}
return match;
}
catch (ManagementException ex)
{
throw new InvalidOperationException("WMI lookup failed. Query: " + queryText + ". Filter: " + filter + ". WMI error: " + ex.Message, ex);
}
} }
public uint InvokeMethod(ManagementObject instance, string methodName, params object[] arguments) public uint InvokeMethod(ManagementObject instance, string methodName, params object[] arguments)
@@ -84,11 +128,20 @@ namespace BizTalkPlatformManagementTool.Services
var methodToCall = ResolveMethodName(instance, methodName); var methodToCall = ResolveMethodName(instance, methodName);
_logger.Info("Calling " + instance.Path.ClassName + "." + methodToCall + " on " + SafeGetString(instance, "Name", SafeGetString(instance, "InstanceName", "<unknown>"))); _logger.Info("Calling " + instance.Path.ClassName + "." + methodToCall + " on " + SafeGetString(instance, "Name", SafeGetString(instance, "InstanceName", "<unknown>")));
object result = instance.InvokeMethod(methodToCall, arguments == null || arguments.Length == 0 ? null : arguments); object result;
try
{
result = instance.InvokeMethod(methodToCall, arguments == null || arguments.Length == 0 ? null : arguments);
}
catch (ManagementException ex)
{
throw new InvalidOperationException("WMI method failed. Class: " + instance.Path.ClassName + ", method: " + methodToCall + ", object: " + SafeObjectName(instance) + ". WMI error: " + ex.Message, ex);
}
var returnCode = ExtractReturnCode(result); var returnCode = ExtractReturnCode(result);
if (returnCode != 0) if (returnCode != 0)
{ {
throw new InvalidOperationException(methodToCall + " returned HRESULT/ReturnValue " + returnCode.ToString(CultureInfo.InvariantCulture)); throw new InvalidOperationException("WMI method returned an error. Class: " + instance.Path.ClassName + ", method: " + methodToCall + ", object: " + SafeObjectName(instance) + ", ReturnValue: " + returnCode.ToString(CultureInfo.InvariantCulture));
} }
return returnCode; return returnCode;
@@ -101,7 +154,7 @@ namespace BizTalkPlatformManagementTool.Services
while (DateTime.UtcNow <= deadline) while (DateTime.UtcNow <= deadline)
{ {
using (var current = FindByProperty(className, keyProperty, keyValue)) using (var current = FindByProperty(className, keyProperty, keyValue, false))
{ {
if (current != null && isReached(current)) if (current != null && isReached(current))
{ {
@@ -113,7 +166,7 @@ namespace BizTalkPlatformManagementTool.Services
Thread.Sleep(TimeSpan.FromSeconds(delay)); Thread.Sleep(TimeSpan.FromSeconds(delay));
} }
throw new TimeoutException("Timeout while waiting for " + description); throw new TimeoutException("Timeout while waiting for " + description + " [" + className + "." + keyProperty + "=" + keyValue + "]");
} }
public static string SafeGetString(ManagementBaseObject item, string propertyName, string fallback) public static string SafeGetString(ManagementBaseObject item, string propertyName, string fallback)
@@ -191,15 +244,60 @@ namespace BizTalkPlatformManagementTool.Services
return false; return false;
} }
private static bool MatchesProperty(ManagementBaseObject item, string className, string propertyName, string expectedValue)
{
foreach (var candidate in CandidatePropertyNames(className, propertyName))
{
if (!HasProperty(item, candidate))
{
continue;
}
var actual = SafeGetString(item, candidate, string.Empty);
if (string.Equals(actual, expectedValue ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static IEnumerable<string> CandidatePropertyNames(string className, string propertyName)
{
yield return propertyName;
if (string.Equals(className, "MSBTS_HostInstance", StringComparison.OrdinalIgnoreCase)
&& string.Equals(propertyName, "InstanceName", StringComparison.OrdinalIgnoreCase))
{
yield return "Name";
}
}
private static string SafeObjectName(ManagementObject instance)
{
try
{
return instance.Path == null ? "<unknown>" : instance.Path.RelativePath;
}
catch
{
return SafeGetString(instance, "Name", SafeGetString(instance, "InstanceName", "<unknown>"));
}
}
private static string ResolveMethodName(ManagementObject instance, string requestedName) private static string ResolveMethodName(ManagementObject instance, string requestedName)
{ {
foreach (MethodData method in instance.Methods) using (var managementClass = new ManagementClass(instance.Scope, new ManagementPath(instance.Path.ClassName), null))
{
foreach (MethodData method in managementClass.Methods)
{ {
if (string.Equals(method.Name, requestedName, StringComparison.OrdinalIgnoreCase)) if (string.Equals(method.Name, requestedName, StringComparison.OrdinalIgnoreCase))
{ {
return method.Name; return method.Name;
} }
} }
}
throw new MissingMethodException(instance.Path.ClassName, requestedName); throw new MissingMethodException(instance.Path.ClassName, requestedName);
} }
@@ -220,11 +318,6 @@ namespace BizTalkPlatformManagementTool.Services
return Convert.ToUInt32(result, CultureInfo.InvariantCulture); return Convert.ToUInt32(result, CultureInfo.InvariantCulture);
} }
private static string EscapeWql(string value)
{
return (value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"");
}
public void Dispose() public void Dispose()
{ {
} }
@@ -1,4 +1,7 @@
using System; using System;
using System.Globalization;
using System.IO;
using System.Threading;
namespace BizTalkPlatformManagementTool.Services namespace BizTalkPlatformManagementTool.Services
{ {
@@ -19,11 +22,28 @@ namespace BizTalkPlatformManagementTool.Services
public sealed class OperationLogger public sealed class OperationLogger
{ {
private const string LogFilePrefix = "BizTalkPlatformManagementTool-";
private const string LogFileExtension = ".log";
private const int RetentionDays = 5;
private static readonly object FileLock = new object();
private static int _cleanupDone;
private readonly Action<LogEntry> _sink; private readonly Action<LogEntry> _sink;
private readonly string _logDirectory;
public OperationLogger(Action<LogEntry> sink) public OperationLogger(Action<LogEntry> sink)
{ {
_sink = sink; _sink = sink;
_logDirectory = AppDomain.CurrentDomain.BaseDirectory;
CleanupOldLogs();
}
public string LogFilePath
{
get
{
return Path.Combine(_logDirectory, LogFilePrefix + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + LogFileExtension);
}
} }
public void Info(string message) public void Info(string message)
@@ -48,17 +68,69 @@ namespace BizTalkPlatformManagementTool.Services
private void Write(LogLevel level, string message) private void Write(LogLevel level, string message)
{ {
var entry = new LogEntry
{
Timestamp = DateTime.Now,
Level = level,
Message = message
};
WriteToFile(entry);
if (_sink == null) if (_sink == null)
{ {
return; return;
} }
_sink(new LogEntry _sink(entry);
}
private void WriteToFile(LogEntry entry)
{ {
Timestamp = DateTime.Now, try
Level = level, {
Message = message var line = string.Format(
}); CultureInfo.InvariantCulture,
"[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
entry.Timestamp,
entry.Level.ToString().ToUpperInvariant(),
entry.Message,
Environment.NewLine);
lock (FileLock)
{
File.AppendAllText(LogFilePath, line);
}
}
catch
{
// Logging must never interrupt BizTalk operations.
}
}
private void CleanupOldLogs()
{
if (Interlocked.Exchange(ref _cleanupDone, 1) == 1)
{
return;
}
try
{
var cutoff = DateTime.Now.Date.AddDays(-(RetentionDays - 1));
foreach (var file in Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension))
{
var lastWrite = File.GetLastWriteTime(file);
if (lastWrite.Date < cutoff)
{
File.Delete(file);
}
}
}
catch
{
// Log retention cleanup is best-effort.
}
} }
} }
} }
@@ -20,6 +20,7 @@ namespace BizTalkPlatformManagementTool.Ui
private readonly DataGridView _logGrid = new DataGridView(); private readonly DataGridView _logGrid = new DataGridView();
private readonly StatusStrip _statusStrip = new StatusStrip(); private readonly StatusStrip _statusStrip = new StatusStrip();
private readonly ToolStripStatusLabel _statusLabel = new ToolStripStatusLabel(); private readonly ToolStripStatusLabel _statusLabel = new ToolStripStatusLabel();
private readonly OperationLogger _logger;
private readonly BizTalkOperationService _service; private readonly BizTalkOperationService _service;
private Button _diagnoseButton; private Button _diagnoseButton;
@@ -37,8 +38,10 @@ namespace BizTalkPlatformManagementTool.Ui
MinimumSize = new Size(980, 640); MinimumSize = new Size(980, 640);
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
_service = new BizTalkOperationService(new OperationLogger(AppendLog)); _logger = new OperationLogger(AppendLog);
_service = new BizTalkOperationService(_logger);
BuildUi(); BuildUi();
_logger.Info("Log file: " + _logger.LogFilePath);
} }
private void BuildUi() private void BuildUi()
@@ -264,12 +267,12 @@ namespace BizTalkPlatformManagementTool.Ui
try try
{ {
work(); work();
AppendLog(new LogEntry { Timestamp = DateTime.Now, Level = LogLevel.Success, Message = "Operation completed." }); _logger.Success("Operation completed.");
SetBusy(false, "Ready."); SetBusy(false, "Ready.");
} }
catch (Exception ex) catch (Exception ex)
{ {
AppendLog(new LogEntry { Timestamp = DateTime.Now, Level = LogLevel.Error, Message = ex.Message }); _logger.Error(FormatException(ex));
SetBusy(false, "Failed: " + ex.Message); SetBusy(false, "Failed: " + ex.Message);
} }
}); });
@@ -458,5 +461,27 @@ namespace BizTalkPlatformManagementTool.Ui
grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells; grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
grid.BackgroundColor = SystemColors.Window; grid.BackgroundColor = SystemColors.Window;
} }
private static string FormatException(Exception ex)
{
if (ex == null)
{
return "Operation failed.";
}
var message = ex.Message;
var inner = ex.InnerException;
while (inner != null)
{
if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0)
{
message += " Inner error: " + inner.Message;
}
inner = inner.InnerException;
}
return message;
}
} }
} }