Add transactional installer and harden runtime operations
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration><Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{74A5D422-0BA5-4559-BD81-C89C071A8FE4}</ProjectGuid><OutputType>Exe</OutputType>
|
||||
<RootNamespace>BizTalkPlatformManagementTool.Packager</RootNamespace><AssemblyName>BizTalkPlatformManagementTool.Packager</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "><DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup>
|
||||
<ItemGroup><Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.IO.Compression" /><Reference Include="System.IO.Compression.FileSystem" /><Reference Include="System.Security" /></ItemGroup>
|
||||
<ItemGroup><Compile Include="Program.cs" /></ItemGroup>
|
||||
<ItemGroup><ProjectReference Include="..\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</Project><Name>BizTalkPlatformManagementTool.Setup</Name></ProjectReference></ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using BizTalkPlatformManagementTool.Setup;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Packager
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Length != 2) throw new ArgumentException("Usage: BizTalkPlatformManagementTool.Packager.exe <repository-root> <configuration>");
|
||||
var root = Path.GetFullPath(args[0]);
|
||||
var configuration = args[1];
|
||||
var artifacts = Path.Combine(root, "artifacts");
|
||||
var package = Path.Combine(artifacts, "BizTalkPlatformManagementTool-Setup");
|
||||
var application = Path.Combine(package, "application");
|
||||
var zip = Path.Combine(artifacts, "BizTalkPlatformManagementTool-Setup.zip");
|
||||
|
||||
if (Directory.Exists(package)) Directory.Delete(package, true);
|
||||
Directory.CreateDirectory(application);
|
||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool.Setup", "bin", configuration, "BizTalkPlatformManagementTool.Setup.exe"), Path.Combine(package, "Setup.exe"));
|
||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe"), Path.Combine(application, "BizTalkPlatformManagementTool.exe"));
|
||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe.config"), Path.Combine(application, "BizTalkPlatformManagementTool.exe.config"));
|
||||
Copy(Path.Combine(root, "Installation.md"), Path.Combine(package, "INSTALLATION.md"));
|
||||
PackageManifest.Write(application, Path.Combine(package, "application.manifest"));
|
||||
PackageManifest.ValidateAndRead(application, Path.Combine(package, "application.manifest"));
|
||||
|
||||
if (File.Exists(zip)) File.Delete(zip);
|
||||
ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false);
|
||||
WriteBase64(zip, zip + ".b64.txt");
|
||||
File.WriteAllText(zip + ".sha256.txt", PackageManifest.Sha256(zip) + " " + Path.GetFileName(zip) + Environment.NewLine, new UTF8Encoding(false));
|
||||
Console.WriteLine("SETUP_PACKAGE=" + package);
|
||||
Console.WriteLine("SETUP_ZIP=" + zip);
|
||||
Console.WriteLine("SETUP_BASE64=" + zip + ".b64.txt");
|
||||
Console.WriteLine("SETUP_SHA256=" + zip + ".sha256.txt");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("Packaging failed: " + ex);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Copy(string source, string target)
|
||||
{
|
||||
if (!File.Exists(source)) throw new FileNotFoundException("Required package file missing: " + source, source);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(target));
|
||||
File.Copy(source, target, true);
|
||||
}
|
||||
|
||||
private static void WriteBase64(string source, string target)
|
||||
{
|
||||
var encoded = Convert.ToBase64String(File.ReadAllBytes(source));
|
||||
var builder = new StringBuilder(encoded.Length + encoded.Length / 64 + 2);
|
||||
for (var offset = 0; offset < encoded.Length; offset += 64)
|
||||
{
|
||||
builder.Append(encoded, offset, Math.Min(64, encoded.Length - offset));
|
||||
builder.Append('\n');
|
||||
}
|
||||
File.WriteAllText(target, builder.ToString(), new UTF8Encoding(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>BizTalkPlatformManagementTool.Setup</RootNamespace>
|
||||
<AssemblyName>BizTalkPlatformManagementTool.Setup</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType><Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Security" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="InstallerEngine.cs" />
|
||||
<Compile Include="MainForm.cs" />
|
||||
<Compile Include="PackageManifest.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="SetupOperationLog.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup><None Include="app.manifest" /></ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,403 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
internal sealed class InstallerEngine
|
||||
{
|
||||
internal const string ApplicationExeName = "BizTalkPlatformManagementTool.exe";
|
||||
private const string ProductName = "BizTalk Platform Management Tool";
|
||||
private const string ProductVersion = "2.1.0";
|
||||
private const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\BizTalkPlatformManagementTool";
|
||||
private readonly string packageDirectory;
|
||||
private readonly string installDirectory;
|
||||
private readonly string dataDirectory;
|
||||
private readonly bool registerWindowsIntegration;
|
||||
private readonly Func<string, bool> selfTestRunner;
|
||||
|
||||
private sealed class WindowsIntegrationSnapshot
|
||||
{
|
||||
public bool RegistryKeyExisted { get; set; }
|
||||
public Dictionary<string, Tuple<object, RegistryValueKind>> RegistryValues { get; set; }
|
||||
public byte[] DesktopShortcut { get; set; }
|
||||
public byte[] StartMenuShortcut { get; set; }
|
||||
public byte[] Uninstaller { get; set; }
|
||||
}
|
||||
|
||||
public InstallerEngine(string packageDirectory)
|
||||
: this(
|
||||
packageDirectory,
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "BizTalkPlatformManagementTool"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "BizTalkPlatformManagementTool"),
|
||||
true,
|
||||
RunApplicationSelfTest)
|
||||
{
|
||||
}
|
||||
|
||||
internal InstallerEngine(string packageDirectory, string installDirectory, string dataDirectory, bool registerWindowsIntegration, Func<string, bool> selfTestRunner)
|
||||
{
|
||||
this.packageDirectory = Path.GetFullPath(packageDirectory);
|
||||
this.installDirectory = Path.GetFullPath(installDirectory);
|
||||
this.dataDirectory = Path.GetFullPath(dataDirectory);
|
||||
this.registerWindowsIntegration = registerWindowsIntegration;
|
||||
this.selfTestRunner = selfTestRunner ?? throw new ArgumentNullException("selfTestRunner");
|
||||
}
|
||||
|
||||
/// <summary>Gets the fixed machine-wide application installation directory.</summary>
|
||||
public string InstallDirectory { get { return installDirectory; } }
|
||||
/// <summary>Gets whether this setup copy contains a complete install/update payload.</summary>
|
||||
public bool HasInstallPayload { get { return Directory.Exists(Path.Combine(packageDirectory, "application")) && File.Exists(Path.Combine(packageDirectory, "application.manifest")); } }
|
||||
/// <summary>Gets whether the application executable is present at the install target.</summary>
|
||||
public bool IsInstalled { get { return File.Exists(Path.Combine(installDirectory, ApplicationExeName)); } }
|
||||
|
||||
/// <summary>Validates, stages and transactionally installs or updates the application.</summary>
|
||||
public void Install(bool createDesktopShortcut, Action<string> report)
|
||||
{
|
||||
report = report ?? delegate { };
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
var log = SetupOperationLog.Create(dataDirectory, "install-update");
|
||||
Action<string> write = message => { log.Write("INFO", message); report(message); };
|
||||
write("Diagnoselog: " + (log.FilePath.Length == 0 ? "nicht verfuegbar" : log.FilePath));
|
||||
|
||||
var sourceApplication = Path.Combine(packageDirectory, "application");
|
||||
var manifestPath = Path.Combine(packageDirectory, "application.manifest");
|
||||
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
|
||||
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
|
||||
var hadExistingInstallation = Directory.Exists(installDirectory);
|
||||
var integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
|
||||
var backupCreated = false;
|
||||
var activated = false;
|
||||
|
||||
try
|
||||
{
|
||||
write("Phase 1/6: Paketmanifest und SHA-256 pruefen.");
|
||||
var files = PackageManifest.ValidateAndRead(sourceApplication, manifestPath);
|
||||
RequirePayload(files, ApplicationExeName);
|
||||
RequirePayload(files, ApplicationExeName + ".config");
|
||||
EnsureApplicationNotRunning();
|
||||
|
||||
write("Phase 2/6: Update in isoliertes Staging kopieren.");
|
||||
CopyPayload(sourceApplication, stagingDirectory, files);
|
||||
var stagedExe = Path.Combine(stagingDirectory, ApplicationExeName);
|
||||
if (!selfTestRunner(stagedExe)) throw new InvalidOperationException("Der WMI-freie Self-Test der Staging-Version ist fehlgeschlagen.");
|
||||
write("Staging-Self-Test erfolgreich.");
|
||||
|
||||
write("Phase 3/6: Vorhandene Version sichern und Staging atomar aktivieren.");
|
||||
if (hadExistingInstallation)
|
||||
{
|
||||
Directory.Move(installDirectory, backupDirectory);
|
||||
backupCreated = true;
|
||||
}
|
||||
Directory.Move(stagingDirectory, installDirectory);
|
||||
activated = true;
|
||||
|
||||
write("Phase 4/6: Aktivierte Version erneut pruefen.");
|
||||
var targetExe = Path.Combine(installDirectory, ApplicationExeName);
|
||||
if (!selfTestRunner(targetExe)) throw new InvalidOperationException("Der Self-Test der aktivierten Version ist fehlgeschlagen.");
|
||||
File.WriteAllText(
|
||||
Path.Combine(installDirectory, "install-state.txt"),
|
||||
"ProductVersion=" + ProductVersion + Environment.NewLine
|
||||
+ "InstalledAt=" + DateTimeOffset.Now.ToString("o", CultureInfo.InvariantCulture) + Environment.NewLine
|
||||
+ "ManifestSha256=" + PackageManifest.Sha256(manifestPath) + Environment.NewLine,
|
||||
new UTF8Encoding(false));
|
||||
|
||||
write("Phase 5/6: Windows-Integration registrieren.");
|
||||
if (registerWindowsIntegration)
|
||||
{
|
||||
RegisterWindowsIntegration(targetExe, createDesktopShortcut);
|
||||
}
|
||||
|
||||
write("Phase 6/6: Backup bereinigen.");
|
||||
TryDeleteDirectory(backupDirectory, write);
|
||||
write("Installation/Update erfolgreich abgeschlossen: " + installDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Write("ERROR", ex.ToString());
|
||||
var rollbackErrors = new List<string>();
|
||||
try
|
||||
{
|
||||
if (activated && Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true);
|
||||
if (backupCreated && Directory.Exists(backupDirectory)) Directory.Move(backupDirectory, installDirectory);
|
||||
write(backupCreated ? "Rollback: vorherige Programmversion wiederhergestellt." : "Rollback: unvollstaendige Neuinstallation entfernt.");
|
||||
}
|
||||
catch (Exception rollbackException)
|
||||
{
|
||||
rollbackErrors.Add(rollbackException.Message);
|
||||
log.Write("ERROR", "Rollback files: " + rollbackException);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (registerWindowsIntegration)
|
||||
{
|
||||
RestoreWindowsIntegration(integrationSnapshot);
|
||||
}
|
||||
}
|
||||
catch (Exception rollbackException)
|
||||
{
|
||||
rollbackErrors.Add(rollbackException.Message);
|
||||
log.Write("ERROR", "Rollback registration: " + rollbackException);
|
||||
}
|
||||
|
||||
TryDeleteDirectory(stagingDirectory, write);
|
||||
TryDeleteDirectory(backupDirectory, write);
|
||||
var suffix = rollbackErrors.Count == 0 ? " Rollback erfolgreich." : " Rollback-Fehler: " + string.Join(" | ", rollbackErrors);
|
||||
throw new InvalidOperationException("Installation/Update fehlgeschlagen." + suffix + " Ursache: " + ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes the active program directory and registered Windows integration.</summary>
|
||||
public void Uninstall(Action<string> report)
|
||||
{
|
||||
report = report ?? delegate { };
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
var log = SetupOperationLog.Create(dataDirectory, "uninstall");
|
||||
Action<string> write = message => { log.Write("INFO", message); report(message); };
|
||||
var integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
|
||||
var removalDirectory = installDirectory + ".removed." + Guid.NewGuid().ToString("N");
|
||||
var filesMoved = false;
|
||||
try
|
||||
{
|
||||
EnsureApplicationNotRunning();
|
||||
if (Directory.Exists(installDirectory))
|
||||
{
|
||||
Directory.Move(installDirectory, removalDirectory);
|
||||
filesMoved = true;
|
||||
}
|
||||
if (registerWindowsIntegration) RemoveWindowsIntegration();
|
||||
TryDeleteDirectory(removalDirectory, write);
|
||||
write("Deinstallation erfolgreich. Installer-Logs bleiben erhalten: " + Path.Combine(dataDirectory, "InstallerLogs"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Write("ERROR", ex.ToString());
|
||||
if (filesMoved && !Directory.Exists(installDirectory) && Directory.Exists(removalDirectory))
|
||||
{
|
||||
try { Directory.Move(removalDirectory, installDirectory); }
|
||||
catch (Exception rollbackException) { log.Write("ERROR", "Uninstall rollback files: " + rollbackException); }
|
||||
}
|
||||
if (registerWindowsIntegration)
|
||||
{
|
||||
try { RestoreWindowsIntegration(integrationSnapshot); }
|
||||
catch (Exception rollbackException) { log.Write("ERROR", "Uninstall rollback registration: " + rollbackException); }
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterWindowsIntegration(string targetExe, bool createDesktopShortcut)
|
||||
{
|
||||
var programsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName);
|
||||
Directory.CreateDirectory(programsDirectory);
|
||||
CreateShortcut(Path.Combine(programsDirectory, ProductName + ".lnk"), targetExe, installDirectory, ProductName);
|
||||
|
||||
if (createDesktopShortcut) CreateShortcut(DesktopShortcutPath, targetExe, installDirectory, ProductName);
|
||||
else if (File.Exists(DesktopShortcutPath)) File.Delete(DesktopShortcutPath);
|
||||
|
||||
var setupDirectory = Path.Combine(dataDirectory, "Setup");
|
||||
Directory.CreateDirectory(setupDirectory);
|
||||
var uninstaller = Path.Combine(setupDirectory, "Uninstall.exe");
|
||||
File.Copy(Assembly.GetExecutingAssembly().Location, uninstaller, true);
|
||||
|
||||
using (var key = Registry.LocalMachine.CreateSubKey(UninstallKeyPath))
|
||||
{
|
||||
if (key == null) throw new InvalidOperationException("Windows uninstall registry key could not be created.");
|
||||
key.SetValue("DisplayName", ProductName);
|
||||
key.SetValue("DisplayVersion", ProductVersion);
|
||||
key.SetValue("Publisher", "BEW");
|
||||
key.SetValue("InstallLocation", installDirectory);
|
||||
key.SetValue("DisplayIcon", targetExe);
|
||||
key.SetValue("UninstallString", "\"" + uninstaller + "\" --uninstall");
|
||||
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveWindowsIntegration()
|
||||
{
|
||||
TryDeleteFile(DesktopShortcutPath);
|
||||
var programsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName);
|
||||
TryDeleteFile(Path.Combine(programsDirectory, ProductName + ".lnk"));
|
||||
if (Directory.Exists(programsDirectory) && Directory.GetFileSystemEntries(programsDirectory).Length == 0) Directory.Delete(programsDirectory);
|
||||
Registry.LocalMachine.DeleteSubKeyTree(UninstallKeyPath, false);
|
||||
}
|
||||
|
||||
private static void CreateShortcut(string shortcutPath, string targetPath, string workingDirectory, string description)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(shortcutPath));
|
||||
var shellType = Type.GetTypeFromProgID("WScript.Shell");
|
||||
if (shellType == null) throw new InvalidOperationException("Windows Script Host is unavailable; shortcut creation failed.");
|
||||
object shell = null;
|
||||
object shortcut = null;
|
||||
try
|
||||
{
|
||||
shell = Activator.CreateInstance(shellType);
|
||||
dynamic dynamicShell = shell;
|
||||
shortcut = dynamicShell.CreateShortcut(shortcutPath);
|
||||
dynamic dynamicShortcut = shortcut;
|
||||
dynamicShortcut.TargetPath = targetPath;
|
||||
dynamicShortcut.WorkingDirectory = workingDirectory;
|
||||
dynamicShortcut.Description = description;
|
||||
dynamicShortcut.IconLocation = targetPath + ",0";
|
||||
dynamicShortcut.Save();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shortcut != null && Marshal.IsComObject(shortcut)) Marshal.FinalReleaseComObject(shortcut);
|
||||
if (shell != null && Marshal.IsComObject(shell)) Marshal.FinalReleaseComObject(shell);
|
||||
}
|
||||
}
|
||||
|
||||
private WindowsIntegrationSnapshot CaptureWindowsIntegration()
|
||||
{
|
||||
var snapshot = new WindowsIntegrationSnapshot
|
||||
{
|
||||
RegistryValues = new Dictionary<string, Tuple<object, RegistryValueKind>>(StringComparer.OrdinalIgnoreCase),
|
||||
DesktopShortcut = ReadFileOrNull(DesktopShortcutPath),
|
||||
StartMenuShortcut = ReadFileOrNull(StartMenuShortcutPath),
|
||||
Uninstaller = ReadFileOrNull(UninstallerPath)
|
||||
};
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(UninstallKeyPath, false))
|
||||
{
|
||||
snapshot.RegistryKeyExisted = key != null;
|
||||
if (key != null)
|
||||
{
|
||||
foreach (var name in key.GetValueNames())
|
||||
{
|
||||
snapshot.RegistryValues[name] = Tuple.Create(key.GetValue(name, null, RegistryValueOptions.DoNotExpandEnvironmentNames), key.GetValueKind(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private void RestoreWindowsIntegration(WindowsIntegrationSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null) return;
|
||||
Registry.LocalMachine.DeleteSubKeyTree(UninstallKeyPath, false);
|
||||
if (snapshot.RegistryKeyExisted)
|
||||
{
|
||||
using (var key = Registry.LocalMachine.CreateSubKey(UninstallKeyPath))
|
||||
{
|
||||
if (key == null) throw new InvalidOperationException("Previous uninstall registry key could not be restored.");
|
||||
foreach (var pair in snapshot.RegistryValues)
|
||||
{
|
||||
key.SetValue(pair.Key, pair.Value.Item1, pair.Value.Item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
RestoreFile(DesktopShortcutPath, snapshot.DesktopShortcut);
|
||||
RestoreFile(StartMenuShortcutPath, snapshot.StartMenuShortcut);
|
||||
RestoreFile(UninstallerPath, snapshot.Uninstaller);
|
||||
}
|
||||
|
||||
private static byte[] ReadFileOrNull(string path)
|
||||
{
|
||||
return File.Exists(path) ? File.ReadAllBytes(path) : null;
|
||||
}
|
||||
|
||||
private static void RestoreFile(string path, byte[] content)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
TryDeleteFile(path);
|
||||
return;
|
||||
}
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
File.WriteAllBytes(path, content);
|
||||
}
|
||||
|
||||
private void EnsureApplicationNotRunning()
|
||||
{
|
||||
var target = Path.Combine(installDirectory, ApplicationExeName);
|
||||
if (!File.Exists(target)) return;
|
||||
foreach (var process in Process.GetProcessesByName(Path.GetFileNameWithoutExtension(ApplicationExeName)))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.Equals(Path.GetFullPath(process.MainModule.FileName), Path.GetFullPath(target), StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException(ProductName + " is still running. Close it before installation or removal.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RunApplicationSelfTest(string executable)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo(executable, "--self-test")
|
||||
{
|
||||
WorkingDirectory = Path.GetDirectoryName(executable),
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using (var process = Process.Start(startInfo))
|
||||
{
|
||||
if (process == null) return false;
|
||||
if (!process.WaitForExit(60000))
|
||||
{
|
||||
try { process.Kill(); } catch { }
|
||||
return false;
|
||||
}
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyPayload(string sourceRoot, string targetRoot, IEnumerable<PackageFile> files)
|
||||
{
|
||||
Directory.CreateDirectory(targetRoot);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var source = PackageManifest.ResolveContainedPath(sourceRoot, file.RelativePath);
|
||||
var target = PackageManifest.ResolveContainedPath(targetRoot, file.RelativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(target));
|
||||
File.Copy(source, target, false);
|
||||
if (!string.Equals(PackageManifest.Sha256(target), file.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("Copied payload failed SHA-256 verification: " + file.RelativePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RequirePayload(IEnumerable<PackageFile> files, string relativePath)
|
||||
{
|
||||
if (!files.Any(x => string.Equals(x.RelativePath, relativePath, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new InvalidDataException("Required payload file is missing from the manifest: " + relativePath);
|
||||
}
|
||||
|
||||
private static void TryDeleteDirectory(string path, Action<string> report)
|
||||
{
|
||||
try { if (Directory.Exists(path)) Directory.Delete(path, true); }
|
||||
catch (Exception ex) { report("WARNUNG: Verzeichnis konnte nicht bereinigt werden: " + path + " - " + ex.Message); }
|
||||
}
|
||||
|
||||
private static void TryDeleteFile(string path)
|
||||
{
|
||||
try { if (File.Exists(path)) File.Delete(path); } catch { }
|
||||
}
|
||||
|
||||
private static string DesktopShortcutPath
|
||||
{
|
||||
get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), ProductName + ".lnk"); }
|
||||
}
|
||||
|
||||
private static string StartMenuShortcutPath
|
||||
{
|
||||
get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName, ProductName + ".lnk"); }
|
||||
}
|
||||
|
||||
private string UninstallerPath
|
||||
{
|
||||
get { return Path.Combine(dataDirectory, "Setup", "Uninstall.exe"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
internal sealed class MainForm : Form
|
||||
{
|
||||
private readonly InstallerEngine engine;
|
||||
private readonly bool uninstallMode;
|
||||
private readonly TextBox output = new TextBox();
|
||||
private readonly CheckBox desktopShortcut = new CheckBox();
|
||||
private readonly Button installButton = new Button();
|
||||
private readonly Button uninstallButton = new Button();
|
||||
private bool busy;
|
||||
|
||||
public MainForm(InstallerEngine engine, bool uninstallMode)
|
||||
{
|
||||
this.engine = engine;
|
||||
this.uninstallMode = uninstallMode;
|
||||
Text = "BizTalk Platform Management Tool Setup";
|
||||
Width = 780;
|
||||
Height = 520;
|
||||
MinimumSize = new Size(680, 420);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
BuildUi();
|
||||
FormClosing += OnFormClosing;
|
||||
}
|
||||
|
||||
private void BuildUi()
|
||||
{
|
||||
var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(16), RowCount = 5, ColumnCount = 1 };
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
root.Controls.Add(new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
||||
Text = "BizTalk Platform Management Tool 2.1.0"
|
||||
});
|
||||
root.Controls.Add(new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Padding = new Padding(0, 8, 0, 8),
|
||||
Text = "Transaktionaler Installer mit SHA-256-Pruefung, Staging-Self-Test und automatischem Rollback.\r\nZiel: " + engine.InstallDirectory
|
||||
});
|
||||
|
||||
desktopShortcut.Text = "Desktop-Verknuepfung fuer alle Benutzer erstellen";
|
||||
desktopShortcut.Checked = true;
|
||||
desktopShortcut.AutoSize = true;
|
||||
desktopShortcut.Enabled = !uninstallMode;
|
||||
root.Controls.Add(desktopShortcut);
|
||||
|
||||
output.Multiline = true;
|
||||
output.ReadOnly = true;
|
||||
output.ScrollBars = ScrollBars.Vertical;
|
||||
output.Dock = DockStyle.Fill;
|
||||
output.Font = new Font(FontFamily.GenericMonospace, 9);
|
||||
root.Controls.Add(output);
|
||||
|
||||
var buttons = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft };
|
||||
var closeButton = new Button { Text = "Schliessen", AutoSize = true };
|
||||
closeButton.Click += (sender, args) => Close();
|
||||
installButton.Text = engine.IsInstalled ? "Update installieren" : "Installieren";
|
||||
installButton.AutoSize = true;
|
||||
installButton.Enabled = engine.HasInstallPayload && !uninstallMode;
|
||||
installButton.Click += (sender, args) => Run(false);
|
||||
uninstallButton.Text = "Deinstallieren";
|
||||
uninstallButton.AutoSize = true;
|
||||
uninstallButton.Enabled = engine.IsInstalled;
|
||||
uninstallButton.Click += (sender, args) => Run(true);
|
||||
buttons.Controls.Add(closeButton);
|
||||
buttons.Controls.Add(uninstallButton);
|
||||
buttons.Controls.Add(installButton);
|
||||
root.Controls.Add(buttons);
|
||||
Controls.Add(root);
|
||||
|
||||
if (uninstallMode) Append("Deinstallationsmodus. Installer-Logs bleiben zu Diagnosezwecken unter ProgramData erhalten.");
|
||||
else if (!engine.HasInstallPayload) Append("Kein Installationspayload neben Setup.exe gefunden. Dieser Aufruf erlaubt nur die Deinstallation.");
|
||||
}
|
||||
|
||||
private void Run(bool uninstall)
|
||||
{
|
||||
if (uninstall && MessageBox.Show(this, "BizTalk Platform Management Tool wirklich deinstallieren?", "Deinstallation bestaetigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
var createDesktopShortcut = desktopShortcut.Checked;
|
||||
SetBusy(true);
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (uninstall) engine.Uninstall(Append);
|
||||
else engine.Install(createDesktopShortcut, Append);
|
||||
Append(uninstall ? "FERTIG: Deinstallation erfolgreich." : "FERTIG: Installation/Update erfolgreich.");
|
||||
Invoke(new Action(() =>
|
||||
{
|
||||
installButton.Text = engine.IsInstalled ? "Update installieren" : "Installieren";
|
||||
uninstallButton.Enabled = engine.IsInstalled;
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Append("FEHLER: " + ex);
|
||||
Invoke(new Action(() => MessageBox.Show(this, ex.Message, "Setup fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBusy(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void Append(string message)
|
||||
{
|
||||
if (IsDisposed || Disposing) return;
|
||||
if (InvokeRequired)
|
||||
{
|
||||
try { BeginInvoke(new Action<string>(Append), message); } catch (InvalidOperationException) { }
|
||||
return;
|
||||
}
|
||||
output.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine);
|
||||
}
|
||||
|
||||
private void SetBusy(bool value)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
try { BeginInvoke(new Action<bool>(SetBusy), value); } catch (InvalidOperationException) { }
|
||||
return;
|
||||
}
|
||||
busy = value;
|
||||
installButton.Enabled = !value && engine.HasInstallPayload && !uninstallMode;
|
||||
uninstallButton.Enabled = !value && engine.IsInstalled;
|
||||
desktopShortcut.Enabled = !value && !uninstallMode;
|
||||
UseWaitCursor = value;
|
||||
}
|
||||
|
||||
private void OnFormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (!busy) return;
|
||||
e.Cancel = true;
|
||||
MessageBox.Show(this, "Das Setup arbeitet noch. Bitte warten Sie bis zum Abschluss.", "Setup laeuft", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
public sealed class PackageFile
|
||||
{
|
||||
/// <summary>Gets or sets the normalized payload-relative path.</summary>
|
||||
public string RelativePath { get; set; }
|
||||
/// <summary>Gets or sets the declared file length.</summary>
|
||||
public long Length { get; set; }
|
||||
/// <summary>Gets or sets the lowercase SHA-256 digest.</summary>
|
||||
public string Sha256 { get; set; }
|
||||
}
|
||||
|
||||
public static class PackageManifest
|
||||
{
|
||||
/// <summary>Reads and cryptographically validates a complete application payload manifest.</summary>
|
||||
public static IList<PackageFile> ValidateAndRead(string applicationDirectory, string manifestPath)
|
||||
{
|
||||
if (!Directory.Exists(applicationDirectory)) throw new DirectoryNotFoundException("Application payload missing: " + applicationDirectory);
|
||||
if (!File.Exists(manifestPath)) throw new FileNotFoundException("Package manifest missing: " + manifestPath, manifestPath);
|
||||
|
||||
var files = new List<PackageFile>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var rawLine in File.ReadAllLines(manifestPath, Encoding.UTF8))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (line.Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) continue;
|
||||
var parts = line.Split(new[] { '|' }, 3);
|
||||
long length;
|
||||
if (parts.Length != 3 || parts[0].Length != 64 || !long.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out length))
|
||||
throw new InvalidDataException("Invalid package manifest line: " + rawLine);
|
||||
|
||||
var relative = NormalizeRelativePath(parts[2]);
|
||||
if (!seen.Add(relative)) throw new InvalidDataException("Duplicate package manifest path: " + relative);
|
||||
var fullPath = ResolveContainedPath(applicationDirectory, relative);
|
||||
if (!File.Exists(fullPath)) throw new FileNotFoundException("Manifest payload file missing: " + relative, fullPath);
|
||||
var info = new FileInfo(fullPath);
|
||||
if (info.Length != length) throw new InvalidDataException("Payload size mismatch: " + relative);
|
||||
var actualHash = Sha256(fullPath);
|
||||
if (!string.Equals(actualHash, parts[0], StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Payload SHA-256 mismatch: " + relative);
|
||||
files.Add(new PackageFile { RelativePath = relative, Length = length, Sha256 = actualHash });
|
||||
}
|
||||
|
||||
if (files.Count == 0) throw new InvalidDataException("The package manifest does not contain payload files.");
|
||||
var actualFiles = Directory.GetFiles(applicationDirectory, "*", SearchOption.AllDirectories)
|
||||
.Select(x => NormalizeRelativePath(x.Substring(Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar).Length + 1)))
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var declaredFiles = files.Select(x => x.RelativePath).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
if (!actualFiles.SequenceEqual(declaredFiles, StringComparer.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("The application payload contains files not covered by the package manifest.");
|
||||
return files;
|
||||
}
|
||||
|
||||
/// <summary>Creates a deterministic manifest covering every application payload file.</summary>
|
||||
public static void Write(string applicationDirectory, string manifestPath)
|
||||
{
|
||||
var root = Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
var lines = Directory.GetFiles(applicationDirectory, "*", SearchOption.AllDirectories)
|
||||
.Select(path => new FileInfo(path))
|
||||
.OrderBy(info => info.FullName, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(info => Sha256(info.FullName) + "|" + info.Length.ToString(CultureInfo.InvariantCulture) + "|" + NormalizeRelativePath(info.FullName.Substring(root.Length)))
|
||||
.ToArray();
|
||||
File.WriteAllLines(manifestPath, lines, new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
/// <summary>Calculates the lowercase SHA-256 digest of a file.</summary>
|
||||
public static string Sha256(string path)
|
||||
{
|
||||
using (var stream = File.OpenRead(path))
|
||||
using (var algorithm = SHA256.Create())
|
||||
{
|
||||
var hash = algorithm.ComputeHash(stream);
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
foreach (var value in hash) builder.Append(value.ToString("x2", CultureInfo.InvariantCulture));
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resolves a relative payload path and rejects directory traversal.</summary>
|
||||
public static string ResolveContainedPath(string root, string relative)
|
||||
{
|
||||
var normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
var result = Path.GetFullPath(Path.Combine(normalizedRoot, NormalizeRelativePath(relative).Replace('/', Path.DirectorySeparatorChar)));
|
||||
if (!result.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Package path escapes the payload root: " + relative);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string NormalizeRelativePath(string path)
|
||||
{
|
||||
path = (path ?? string.Empty).Replace('\\', '/').Trim();
|
||||
if (path.Length == 0 || path.StartsWith("/", StringComparison.Ordinal) || path.Contains("../") || path == ".." || Path.IsPathRooted(path))
|
||||
throw new InvalidDataException("Unsafe package path: " + path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
var uninstallMode = args != null && args.Length == 1 && string.Equals(args[0], "--uninstall", StringComparison.OrdinalIgnoreCase);
|
||||
Application.Run(new MainForm(new InstallerEngine(AppDomain.CurrentDomain.BaseDirectory), uninstallMode));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("BizTalk Platform Management Tool Setup")]
|
||||
[assembly: AssemblyDescription("Transactional installer and updater for BizTalk Platform Management Tool")]
|
||||
[assembly: AssemblyCompany("BEW")]
|
||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
|
||||
[assembly: AssemblyVersion("2.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.0.0")]
|
||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
internal sealed class SetupOperationLog
|
||||
{
|
||||
private readonly object sync = new object();
|
||||
|
||||
private SetupOperationLog(string filePath)
|
||||
{
|
||||
FilePath = filePath;
|
||||
}
|
||||
|
||||
public string FilePath { get; private set; }
|
||||
|
||||
public static SetupOperationLog Create(string dataDirectory, string operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.Combine(dataDirectory, "InstallerLogs");
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = Path.Combine(directory, "setup-" + DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + "-" + operation + ".log");
|
||||
return new SetupOperationLog(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new SetupOperationLog(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string level, string message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath)) return;
|
||||
var line = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) + "][" + level + "] " + (message ?? string.Empty) + Environment.NewLine;
|
||||
try
|
||||
{
|
||||
lock (sync) File.AppendAllText(FilePath, line, new UTF8Encoding(false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Setup logging must not hide the actual installation result.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="2.1.0.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application><supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /></application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -12,6 +12,7 @@
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@@ -45,7 +46,9 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="RuntimeSelfTest.cs" />
|
||||
<Compile Include="Models\ArtifactStates.cs" />
|
||||
<Compile Include="Models\BizTalkSnapshot.cs" />
|
||||
<Compile Include="Models\DiffModels.cs" />
|
||||
@@ -56,12 +59,14 @@
|
||||
<Compile Include="Services\JsonFileStore.cs" />
|
||||
<Compile Include="Services\OperationLogger.cs" />
|
||||
<Compile Include="Services\SnapshotComparer.cs" />
|
||||
<Compile Include="Services\SnapshotValidator.cs" />
|
||||
<Compile Include="Services\SnapshotStore.cs" />
|
||||
<Compile Include="Services\BizTalkOperationService.cs" />
|
||||
<Compile Include="Ui\MainForm.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -1,26 +1,81 @@
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains BizTalk WMI state constants and display helpers used by snapshots,
|
||||
/// plans and reports.
|
||||
/// </summary>
|
||||
public static class ArtifactStates
|
||||
{
|
||||
/// <summary>
|
||||
/// WMI status value for a bound send port.
|
||||
/// </summary>
|
||||
public const int SendPortBound = 1;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a stopped send port.
|
||||
/// </summary>
|
||||
public const int SendPortStopped = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a started send port.
|
||||
/// </summary>
|
||||
public const int SendPortStarted = 3;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for an unbound orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationUnbound = 1;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a bound orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationBound = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a stopped orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationStopped = 3;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a started orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationStarted = 4;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a stopped host instance.
|
||||
/// </summary>
|
||||
public const int HostStopped = 1;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a host instance that is starting.
|
||||
/// </summary>
|
||||
public const int HostStartPending = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a host instance that is stopping.
|
||||
/// </summary>
|
||||
public const int HostStopPending = 3;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a started host instance.
|
||||
/// </summary>
|
||||
public const int HostStarted = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a receive location enabled flag into the text used in reports.
|
||||
/// </summary>
|
||||
/// <param name="enabled">True when the receive location is enabled.</param>
|
||||
/// <returns>A display value for the receive location state.</returns>
|
||||
public static string FormatReceiveLocation(bool enabled)
|
||||
{
|
||||
return enabled ? "Enabled" : "Disabled";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an MSBTS_SendPort.Status value into a display string.
|
||||
/// </summary>
|
||||
/// <param name="status">The raw WMI send port status value.</param>
|
||||
/// <returns>A known status name or an Unknown value with the raw code.</returns>
|
||||
public static string FormatSendPort(int status)
|
||||
{
|
||||
switch (status)
|
||||
@@ -32,6 +87,11 @@ namespace BizTalkPlatformManagementTool.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an MSBTS_Orchestration.OrchestrationStatus value into a display string.
|
||||
/// </summary>
|
||||
/// <param name="status">The raw WMI orchestration status value.</param>
|
||||
/// <returns>A known status name or an Unknown value with the raw code.</returns>
|
||||
public static string FormatOrchestration(int status)
|
||||
{
|
||||
switch (status)
|
||||
@@ -44,6 +104,11 @@ namespace BizTalkPlatformManagementTool.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an MSBTS_HostInstance.ServiceState value into a display string.
|
||||
/// </summary>
|
||||
/// <param name="state">The raw WMI host instance service state value.</param>
|
||||
/// <returns>A known service state name or an Unknown value with the raw code.</returns>
|
||||
public static string FormatHostInstance(int state)
|
||||
{
|
||||
switch (state)
|
||||
|
||||
@@ -3,34 +3,62 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents one captured BizTalk runtime state including application artifacts
|
||||
/// and host instances.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class BizTalkSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new snapshot with empty application and host instance collections.
|
||||
/// </summary>
|
||||
public BizTalkSnapshot()
|
||||
{
|
||||
Applications = new List<ApplicationSnapshot>();
|
||||
HostInstances = new List<HostInstanceState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tool version that created the snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string ToolVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local timestamp when the snapshot was created.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk server name used for the WMI connection.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk application snapshots captured from WMI.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public List<ApplicationSnapshot> Applications { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host instances captured from the BizTalk group.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public List<HostInstanceState> HostInstances { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups the captured artifact states for one BizTalk application.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class ApplicationSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an application snapshot with empty artifact collections.
|
||||
/// </summary>
|
||||
public ApplicationSnapshot()
|
||||
{
|
||||
ReceiveLocations = new List<ReceiveLocationState>();
|
||||
@@ -38,88 +66,169 @@ namespace BizTalkPlatformManagementTool.Models
|
||||
Orchestrations = new List<OrchestrationState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk application name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive locations that belong to the application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<ReceiveLocationState> ReceiveLocations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the send ports that belong to the application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public List<SendPortState> SendPorts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orchestrations that belong to the application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public List<OrchestrationState> Orchestrations { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk receive location.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class ReceiveLocationState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive location name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent receive port name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string ReceivePortName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the receive location is enabled.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive adapter name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string AdapterName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive location transport address.
|
||||
/// </summary>
|
||||
[DataMember(Order = 6)]
|
||||
public string Address { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk send port.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class SendPortState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the send port name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw MSBTS_SendPort.Status value.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary transport adapter type.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string PrimaryTransportType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary transport address.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string PrimaryTransportAddress { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk orchestration.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OrchestrationState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orchestration name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw MSBTS_Orchestration.OrchestrationStatus value.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public int OrchestrationStatus { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk host instance.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class HostInstanceState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the host instance name used as the WMI key.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string InstanceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk host name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string HostName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server on which the host instance runs.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw MSBTS_HostInstance.ServiceState value.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public int RawState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted host instance state.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string StateText { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,56 +3,104 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains all differences detected between two BizTalk snapshots.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class SnapshotDiff
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new diff with empty artifact and host instance collections.
|
||||
/// </summary>
|
||||
public SnapshotDiff()
|
||||
{
|
||||
ArtifactDifferences = new List<ArtifactDiffEntry>();
|
||||
HostInstanceDifferences = new List<HostInstanceDiffEntry>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets state differences for receive locations, send ports and orchestrations.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public List<ArtifactDiffEntry> ArtifactDifferences { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets state differences for host instances.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<HostInstanceDiffEntry> HostInstanceDifferences { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a before/after state change for one BizTalk application artifact.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class ArtifactDiffEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact category, such as SendPort or ReceiveLocation.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string ArtifactType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the before snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Before { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the after snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string After { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a before/after state change for one BizTalk host instance.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class HostInstanceDiffEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the host instance name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string InstanceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk host name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string HostName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server on which the host instance runs.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the before snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Before { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the after snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string After { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,92 +3,211 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the supported operation plan modes.
|
||||
/// </summary>
|
||||
public enum OperationMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Plan mode for stopping BizTalk runtime artifacts before maintenance.
|
||||
/// </summary>
|
||||
Shutdown,
|
||||
|
||||
/// <summary>
|
||||
/// Plan mode for returning BizTalk runtime artifacts to a captured state.
|
||||
/// </summary>
|
||||
Restore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the artifact categories that can appear in an operation plan.
|
||||
/// </summary>
|
||||
public enum OperationStepKind
|
||||
{
|
||||
/// <summary>
|
||||
/// A receive location step.
|
||||
/// </summary>
|
||||
ReceiveLocation,
|
||||
|
||||
/// <summary>
|
||||
/// A send port step.
|
||||
/// </summary>
|
||||
SendPort,
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration step.
|
||||
/// </summary>
|
||||
Orchestration,
|
||||
|
||||
/// <summary>
|
||||
/// A host instance step.
|
||||
/// </summary>
|
||||
HostInstance,
|
||||
|
||||
/// <summary>
|
||||
/// An informational step that is intentionally not executed.
|
||||
/// </summary>
|
||||
Note
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the ordered shutdown or restore plan written before any
|
||||
/// runtime-changing operation is executed.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OperationPlan
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new operation plan with an empty step collection.
|
||||
/// </summary>
|
||||
public OperationPlan()
|
||||
{
|
||||
Steps = new List<OperationStep>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the plan mode as a serialized string.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local timestamp when the plan was created.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target server for server-scoped plan steps.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ordered operation steps.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public List<OperationStep> Steps { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes one executable or informational step in a shutdown or restore plan.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OperationStep
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact kind for this step.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Kind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application, when applicable.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact or host instance name displayed to the user.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server associated with the step, when server scoped.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the human-readable action description.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI class used to resolve the runtime object.
|
||||
/// </summary>
|
||||
[DataMember(Order = 6)]
|
||||
public string WmiClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI key property used to locate the runtime object.
|
||||
/// </summary>
|
||||
[DataMember(Order = 7)]
|
||||
public string KeyProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI key value used to locate the runtime object.
|
||||
/// </summary>
|
||||
[DataMember(Order = 8)]
|
||||
public string KeyValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI method or service pseudo-method to execute.
|
||||
/// </summary>
|
||||
[DataMember(Order = 9)]
|
||||
public string MethodName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the numeric arguments passed to the WMI method.
|
||||
/// </summary>
|
||||
[DataMember(Order = 10)]
|
||||
public int[] Arguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw WMI state expected after the method completes.
|
||||
/// </summary>
|
||||
[DataMember(Order = 11)]
|
||||
public int? TargetState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the step should be executed.
|
||||
/// </summary>
|
||||
[DataMember(Order = 12)]
|
||||
public bool Execute { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an operator-facing warning for skipped or risky steps.
|
||||
/// </summary>
|
||||
[DataMember(Order = 13)]
|
||||
public string Warning { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains user-selected runtime options for snapshot, shutdown and restore actions.
|
||||
/// </summary>
|
||||
public sealed class OperationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk server used for WMI operations.
|
||||
/// </summary>
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the directory where snapshots, plans, reports and diffs are written.
|
||||
/// </summary>
|
||||
public string OutputDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state file used as restore input.
|
||||
/// </summary>
|
||||
public string StateFile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether operations should only be logged.
|
||||
/// </summary>
|
||||
public bool DryRun { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of seconds to wait for a target runtime state.
|
||||
/// </summary>
|
||||
public int WaitTimeoutSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of seconds between WMI polling attempts.
|
||||
/// </summary>
|
||||
public int PollIntervalSeconds { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using BizTalkPlatformManagementTool.Ui;
|
||||
|
||||
namespace BizTalkPlatformManagementTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the WinForms application entry point and startup guard checks.
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts the application after verifying that BizTalk WMI operations can run elevated.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
if (args != null && args.Length == 1 && string.Equals(args[0], "--self-test", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RuntimeSelfTest.Run();
|
||||
}
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
@@ -22,12 +34,32 @@ namespace BizTalkPlatformManagementTool
|
||||
"Administrator Rights Required",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
Application.Run(new MainForm());
|
||||
bool createdNew;
|
||||
using (var mutex = new Mutex(true, @"Local\BizTalkPlatformManagementTool", out createdNew))
|
||||
{
|
||||
if (!createdNew)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"BizTalk Platform Management Tool is already running in this Windows session.",
|
||||
"BizTalk Platform Management Tool",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Application.Run(new MainForm());
|
||||
GC.KeepAlive(mutex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the current Windows identity is a local administrator.
|
||||
/// </summary>
|
||||
/// <returns>True when the process is elevated as administrator; otherwise false.</returns>
|
||||
private static bool IsRunningAsAdministrator()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("BizTalk Platform Management Tool")]
|
||||
[assembly: AssemblyDescription("Controlled BizTalk Server maintenance snapshots, plans and runtime operations")]
|
||||
[assembly: AssemblyCompany("BEW")]
|
||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
||||
[assembly: AssemblyVersion("2.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.0.0")]
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using BizTalkPlatformManagementTool.Models;
|
||||
using BizTalkPlatformManagementTool.Services;
|
||||
|
||||
namespace BizTalkPlatformManagementTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a WMI-free smoke test used by the installer before and after activation.
|
||||
/// </summary>
|
||||
internal static class RuntimeSelfTest
|
||||
{
|
||||
public static int Run()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.SelfTest." + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
var before = SampleSnapshot(ArtifactStates.SendPortStarted);
|
||||
var after = SampleSnapshot(ArtifactStates.SendPortStopped);
|
||||
var snapshotPath = Path.Combine(directory, "snapshot.json");
|
||||
JsonFileStore.Save(snapshotPath, before);
|
||||
var loaded = JsonFileStore.Load<BizTalkSnapshot>(snapshotPath);
|
||||
SnapshotValidator.Validate(loaded);
|
||||
|
||||
var diff = SnapshotComparer.Compare(loaded, after);
|
||||
if (diff.ArtifactDifferences.Count != 1)
|
||||
{
|
||||
throw new InvalidOperationException("Snapshot diff self-test returned an unexpected result.");
|
||||
}
|
||||
|
||||
SnapshotStore.SaveSnapshotSet(Path.Combine(directory, "before.json"), loaded);
|
||||
SnapshotStore.SaveDiffSet(Path.Combine(directory, "diff.json"), diff);
|
||||
Console.WriteLine("SELF_TEST_OK version=" + BizTalkOperationService.Version);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("SELF_TEST_FAILED " + ex);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(directory))
|
||||
{
|
||||
Directory.Delete(directory, true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// The self-test result is more important than temporary cleanup.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BizTalkSnapshot SampleSnapshot(int sendPortState)
|
||||
{
|
||||
var snapshot = new BizTalkSnapshot
|
||||
{
|
||||
ToolVersion = BizTalkOperationService.Version,
|
||||
CreatedAt = DateTimeOffset.Now.ToString("o"),
|
||||
Server = Environment.MachineName
|
||||
};
|
||||
var app = new ApplicationSnapshot { Application = "SelfTest" };
|
||||
app.SendPorts.Add(new SendPortState
|
||||
{
|
||||
Application = app.Application,
|
||||
Name = "SelfTest.SendPort",
|
||||
Status = sendPortState
|
||||
});
|
||||
snapshot.Applications.Add(app);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,27 +7,60 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Coordinates BizTalk snapshot, plan, shutdown, restore and persistence operations.
|
||||
/// </summary>
|
||||
public sealed class BizTalkOperationService
|
||||
{
|
||||
public const string Version = "2.0.0-net461";
|
||||
/// <summary>
|
||||
/// Current tool version written into generated snapshots.
|
||||
/// </summary>
|
||||
public const string Version = "2.1.0-net461";
|
||||
|
||||
/// <summary>
|
||||
/// Fallback application name used when WMI does not expose an application property.
|
||||
/// </summary>
|
||||
private const string UnknownApplication = "(Unknown Application)";
|
||||
|
||||
/// <summary>
|
||||
/// Logger used for all operator-facing operation messages.
|
||||
/// </summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new operation service.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger used for operator-visible progress and diagnostics.</param>
|
||||
public BizTalkOperationService(OperationLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the BizTalk WMI namespace is reachable and readable.
|
||||
/// </summary>
|
||||
/// <param name="server">The BizTalk server or management host to query.</param>
|
||||
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);
|
||||
try
|
||||
{
|
||||
_logger.Success("WMI/CIM diagnostic succeeded. Send ports visible: " + ports.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(ports);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the current BizTalk runtime state from WMI.
|
||||
/// </summary>
|
||||
/// <param name="server">The BizTalk server or management host to query.</param>
|
||||
/// <returns>A complete snapshot of supported BizTalk artifacts and host instances.</returns>
|
||||
public BizTalkSnapshot CreateSnapshot(string server)
|
||||
{
|
||||
using (var client = CreateClient(server))
|
||||
@@ -35,63 +68,95 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
var snapshot = new BizTalkSnapshot
|
||||
{
|
||||
ToolVersion = Version,
|
||||
CreatedAt = DateTime.Now.ToString("s"),
|
||||
CreatedAt = DateTimeOffset.Now.ToString("o"),
|
||||
Server = client.Server
|
||||
};
|
||||
|
||||
var apps = new Dictionary<string, ApplicationSnapshot>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var item in client.Query("MSBTS_ReceiveLocation"))
|
||||
var receiveLocations = client.Query("MSBTS_ReceiveLocation");
|
||||
try
|
||||
{
|
||||
var appName = GetApplicationName(item);
|
||||
var app = GetApplication(apps, appName);
|
||||
app.ReceiveLocations.Add(new ReceiveLocationState
|
||||
foreach (var item in receiveLocations)
|
||||
{
|
||||
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)
|
||||
});
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(receiveLocations);
|
||||
}
|
||||
|
||||
foreach (var item in client.Query("MSBTS_SendPort"))
|
||||
var sendPorts = client.Query("MSBTS_SendPort");
|
||||
try
|
||||
{
|
||||
var appName = GetApplicationName(item);
|
||||
var app = GetApplication(apps, appName);
|
||||
app.SendPorts.Add(new SendPortState
|
||||
foreach (var item in sendPorts)
|
||||
{
|
||||
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)
|
||||
});
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(sendPorts);
|
||||
}
|
||||
|
||||
foreach (var item in client.Query("MSBTS_Orchestration"))
|
||||
var orchestrations = client.Query("MSBTS_Orchestration");
|
||||
try
|
||||
{
|
||||
var appName = GetApplicationName(item);
|
||||
var app = GetApplication(apps, appName);
|
||||
app.Orchestrations.Add(new OrchestrationState
|
||||
foreach (var item in orchestrations)
|
||||
{
|
||||
Application = app.Application,
|
||||
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
|
||||
OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0)
|
||||
});
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(orchestrations);
|
||||
}
|
||||
|
||||
foreach (var item in client.Query("MSBTS_HostInstance"))
|
||||
var hostInstances = client.Query("MSBTS_HostInstance");
|
||||
try
|
||||
{
|
||||
var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0);
|
||||
snapshot.HostInstances.Add(new HostInstanceState
|
||||
foreach (var item in hostInstances)
|
||||
{
|
||||
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)
|
||||
});
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(hostInstances);
|
||||
}
|
||||
|
||||
foreach (var app in apps.Values.OrderBy(a => a.Application))
|
||||
@@ -103,13 +168,21 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
snapshot.HostInstances = snapshot.HostInstances.OrderBy(x => x.Server).ThenBy(x => x.InstanceName).ToList();
|
||||
|
||||
SnapshotValidator.Validate(snapshot);
|
||||
_logger.Success("Snapshot created. Applications: " + snapshot.Applications.Count + ", host instances: " + snapshot.HostInstances.Count);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ordered shutdown plan from a snapshot.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The runtime state used as the source for the plan.</param>
|
||||
/// <param name="server">The selected server on which host instance steps may execute.</param>
|
||||
/// <returns>An ordered shutdown plan.</returns>
|
||||
public OperationPlan CreateShutdownPlan(BizTalkSnapshot snapshot, string server)
|
||||
{
|
||||
SnapshotValidator.EnsureServerMatches(snapshot, server);
|
||||
var plan = NewPlan(OperationMode.Shutdown, server);
|
||||
|
||||
foreach (var app in snapshot.Applications)
|
||||
@@ -144,8 +217,15 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return plan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ordered restore plan from a previously captured snapshot.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The state that should be restored.</param>
|
||||
/// <param name="server">The selected server on which host instance steps may execute.</param>
|
||||
/// <returns>An ordered restore plan.</returns>
|
||||
public OperationPlan CreateRestorePlan(BizTalkSnapshot snapshot, string server)
|
||||
{
|
||||
SnapshotValidator.EnsureServerMatches(snapshot, server);
|
||||
var plan = NewPlan(OperationMode.Restore, server);
|
||||
|
||||
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
|
||||
@@ -214,8 +294,21 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return plan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an operation plan or logs each step when dry-run mode is enabled.
|
||||
/// </summary>
|
||||
/// <param name="plan">The ordered plan to execute.</param>
|
||||
/// <param name="options">The runtime options controlling server, dry-run and wait behavior.</param>
|
||||
public void ExecutePlan(OperationPlan plan, OperationOptions options)
|
||||
{
|
||||
if (plan == null || options == null)
|
||||
{
|
||||
throw new ArgumentNullException(plan == null ? "plan" : "options");
|
||||
}
|
||||
if (!SnapshotValidator.ServerNamesEqual(plan.Server, options.Server))
|
||||
{
|
||||
throw new InvalidOperationException("The operation plan targets server '" + plan.Server + "' but execution was requested for '" + options.Server + "'.");
|
||||
}
|
||||
using (var client = CreateClient(options.Server))
|
||||
{
|
||||
foreach (var step in plan.Steps)
|
||||
@@ -253,6 +346,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a snapshot and its report sidecars.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">The directory where output files are written.</param>
|
||||
/// <param name="fileName">The primary JSON file name.</param>
|
||||
/// <param name="snapshot">The snapshot to persist.</param>
|
||||
/// <returns>The primary JSON file path.</returns>
|
||||
public string SaveSnapshot(string outputDirectory, string fileName, BizTalkSnapshot snapshot)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
@@ -262,6 +362,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves an operation plan as JSON.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">The directory where output files are written.</param>
|
||||
/// <param name="fileName">The plan JSON file name.</param>
|
||||
/// <param name="plan">The plan to persist.</param>
|
||||
/// <returns>The saved plan file path.</returns>
|
||||
public string SavePlan(string outputDirectory, string fileName, OperationPlan plan)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
@@ -271,6 +378,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a diff and its report sidecars.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">The directory where output files are written.</param>
|
||||
/// <param name="fileName">The primary diff JSON file name.</param>
|
||||
/// <param name="diff">The diff to persist.</param>
|
||||
/// <returns>The primary diff JSON file path.</returns>
|
||||
public string SaveDiff(string outputDirectory, string fileName, SnapshotDiff diff)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
@@ -280,6 +394,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one concrete WMI operation step and waits for its target state.
|
||||
/// </summary>
|
||||
/// <param name="client">The connected WMI client.</param>
|
||||
/// <param name="instance">The resolved WMI object for the step.</param>
|
||||
/// <param name="step">The operation step to execute.</param>
|
||||
/// <param name="options">The runtime options controlling wait behavior.</param>
|
||||
private void ExecuteStep(BizTalkWmiClient client, ManagementObject instance, OperationStep step, OperationOptions options)
|
||||
{
|
||||
if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -318,6 +439,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
WaitForTarget(client, step, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits until a WMI object reaches the target state described by a plan step.
|
||||
/// </summary>
|
||||
/// <param name="client">The connected WMI client.</param>
|
||||
/// <param name="step">The step whose target state should be verified.</param>
|
||||
/// <param name="options">The runtime options controlling timeout and polling interval.</param>
|
||||
private void WaitForTarget(BizTalkWmiClient client, OperationStep step, OperationOptions options)
|
||||
{
|
||||
if (!step.TargetState.HasValue)
|
||||
@@ -344,6 +471,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and connects a WMI client for a server.
|
||||
/// </summary>
|
||||
/// <param name="server">The BizTalk server or management host to connect to.</param>
|
||||
/// <returns>A connected WMI client.</returns>
|
||||
private BizTalkWmiClient CreateClient(string server)
|
||||
{
|
||||
var client = new BizTalkWmiClient(server, _logger);
|
||||
@@ -351,16 +483,37 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new empty operation plan with common metadata.
|
||||
/// </summary>
|
||||
/// <param name="mode">The operation mode represented by the plan.</param>
|
||||
/// <param name="server">The target server stored in the plan metadata.</param>
|
||||
/// <returns>A new operation plan.</returns>
|
||||
private static OperationPlan NewPlan(OperationMode mode, string server)
|
||||
{
|
||||
return new OperationPlan
|
||||
{
|
||||
Mode = mode.ToString(),
|
||||
CreatedAt = DateTime.Now.ToString("s"),
|
||||
CreatedAt = DateTimeOffset.Now.ToString("o"),
|
||||
Server = server
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates one executable operation step.
|
||||
/// </summary>
|
||||
/// <param name="kind">The artifact kind displayed and serialized for the step.</param>
|
||||
/// <param name="application">The owning BizTalk application, when applicable.</param>
|
||||
/// <param name="name">The artifact or host instance name.</param>
|
||||
/// <param name="server">The server associated with the step, when applicable.</param>
|
||||
/// <param name="action">The human-readable action text.</param>
|
||||
/// <param name="wmiClass">The WMI class used to resolve the target object.</param>
|
||||
/// <param name="keyProperty">The WMI key property used for lookup.</param>
|
||||
/// <param name="keyValue">The WMI key value used for lookup.</param>
|
||||
/// <param name="methodName">The WMI method or pseudo-method to execute.</param>
|
||||
/// <param name="arguments">Optional numeric WMI method arguments.</param>
|
||||
/// <param name="targetState">Optional raw WMI state expected after execution.</param>
|
||||
/// <returns>A configured operation step.</returns>
|
||||
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
|
||||
@@ -380,6 +533,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts integer method arguments into the object array required by WMI.
|
||||
/// </summary>
|
||||
/// <param name="values">The integer values from the operation step.</param>
|
||||
/// <returns>An object array suitable for ManagementObject.InvokeMethod.</returns>
|
||||
private static object[] ToObjects(int[] values)
|
||||
{
|
||||
if (values == null || values.Length == 0)
|
||||
@@ -395,6 +553,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an operator-facing description for one plan step.
|
||||
/// </summary>
|
||||
/// <param name="step">The step to describe.</param>
|
||||
/// <returns>A compact step description for logs and errors.</returns>
|
||||
private static string DescribeStep(OperationStep step)
|
||||
{
|
||||
if (step == null)
|
||||
@@ -412,6 +575,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
+ "]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates an application snapshot bucket in a dictionary.
|
||||
/// </summary>
|
||||
/// <param name="apps">The application snapshot dictionary keyed by application name.</param>
|
||||
/// <param name="appName">The application name to resolve.</param>
|
||||
/// <returns>The existing or newly created application snapshot.</returns>
|
||||
private static ApplicationSnapshot GetApplication(Dictionary<string, ApplicationSnapshot> apps, string appName)
|
||||
{
|
||||
ApplicationSnapshot app;
|
||||
@@ -423,6 +592,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the application name from the available WMI properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object being mapped into a snapshot item.</param>
|
||||
/// <returns>The discovered application name or a stable fallback.</returns>
|
||||
private static string GetApplicationName(ManagementObject item)
|
||||
{
|
||||
var names = new[] { "ApplicationName", "Application", "BizTalkApplication" };
|
||||
@@ -436,5 +610,20 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
return UnknownApplication;
|
||||
}
|
||||
|
||||
private static void DisposeAll(IEnumerable<ManagementObject> items)
|
||||
{
|
||||
if (items == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,53 @@ using System.Threading;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides the WMI access layer for BizTalk Server objects in root\MicrosoftBizTalkServer.
|
||||
/// </summary>
|
||||
public sealed class BizTalkWmiClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// BizTalk WMI namespace used for all platform management queries.
|
||||
/// </summary>
|
||||
private const string NamespacePath = "root\\MicrosoftBizTalkServer";
|
||||
|
||||
/// <summary>
|
||||
/// Target server for the WMI connection.
|
||||
/// </summary>
|
||||
private readonly string _server;
|
||||
|
||||
/// <summary>
|
||||
/// Logger used for WMI diagnostics and operation traces.
|
||||
/// </summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Connected WMI management scope, created lazily or by Connect.
|
||||
/// </summary>
|
||||
private ManagementScope _scope;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new WMI client for the specified server.
|
||||
/// </summary>
|
||||
/// <param name="server">The target server name, or an empty value to use the local machine.</param>
|
||||
/// <param name="logger">The operation logger used for diagnostics and trace output.</param>
|
||||
public BizTalkWmiClient(string server, OperationLogger logger)
|
||||
{
|
||||
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the normalized target server name used by this client.
|
||||
/// </summary>
|
||||
public string Server
|
||||
{
|
||||
get { return _server; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the BizTalk WMI namespace on the target server.
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
var path = "\\\\" + _server + "\\" + NamespacePath;
|
||||
@@ -33,11 +62,22 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
_logger.Success("WMI connection established.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a broad WMI query for all instances of the requested BizTalk class.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <returns>The matching WMI objects. The caller owns the returned objects.</returns>
|
||||
public List<ManagementObject> Query(string className)
|
||||
{
|
||||
return Query(className, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a broad WMI query and optionally logs the query text.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="logQuery">True to write the query to the operation log.</param>
|
||||
/// <returns>The matching WMI objects. The caller owns the returned objects.</returns>
|
||||
private List<ManagementObject> Query(string className, bool logQuery)
|
||||
{
|
||||
EnsureConnected();
|
||||
@@ -63,17 +103,37 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
{
|
||||
foreach (var item in result)
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
throw new InvalidOperationException("WMI query failed. Query: " + queryText + ". WMI error: " + ex.Message, ex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds one WMI object by comparing a property value client-side.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="propertyName">The property used as the lookup key.</param>
|
||||
/// <param name="value">The expected property value.</param>
|
||||
/// <returns>The matching object, or null when no object matches.</returns>
|
||||
public ManagementObject FindByProperty(string className, string propertyName, string value)
|
||||
{
|
||||
return FindByProperty(className, propertyName, value, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds one WMI object using SELECT * plus a client-side filter so special
|
||||
/// characters in BizTalk names cannot break a WQL WHERE clause.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="propertyName">The property used as the lookup key.</param>
|
||||
/// <param name="value">The expected property value.</param>
|
||||
/// <param name="logLookup">True to write the lookup details to the operation log.</param>
|
||||
/// <returns>The matching object, or null when no object matches.</returns>
|
||||
private ManagementObject FindByProperty(string className, string propertyName, string value, bool logLookup)
|
||||
{
|
||||
EnsureConnected();
|
||||
@@ -118,6 +178,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a WMI method and validates that the method returned success.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WMI object on which the method should be called.</param>
|
||||
/// <param name="methodName">The method name to invoke.</param>
|
||||
/// <param name="arguments">Optional method arguments.</param>
|
||||
/// <returns>The WMI return code.</returns>
|
||||
public uint InvokeMethod(ManagementObject instance, string methodName, params object[] arguments)
|
||||
{
|
||||
if (instance == null)
|
||||
@@ -138,15 +205,36 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
throw new InvalidOperationException("WMI method failed. Class: " + instance.Path.ClassName + ", method: " + methodToCall + ", object: " + SafeObjectName(instance) + ". WMI error: " + ex.Message, ex);
|
||||
}
|
||||
|
||||
var returnCode = ExtractReturnCode(result);
|
||||
if (returnCode != 0)
|
||||
var output = result as ManagementBaseObject;
|
||||
try
|
||||
{
|
||||
throw new InvalidOperationException("WMI method returned an error. Class: " + instance.Path.ClassName + ", method: " + methodToCall + ", object: " + SafeObjectName(instance) + ", ReturnValue: " + returnCode.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
var returnCode = ExtractReturnCode(result);
|
||||
if (returnCode != 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (output != null)
|
||||
{
|
||||
output.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls one WMI object until it reaches the expected state or times out.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="keyProperty">The WMI key property used to find the object.</param>
|
||||
/// <param name="keyValue">The WMI key value used to find the object.</param>
|
||||
/// <param name="isReached">Predicate that returns true when the state is reached.</param>
|
||||
/// <param name="description">Human-readable state description used in logs and errors.</param>
|
||||
/// <param name="timeoutSeconds">Maximum number of seconds to wait.</param>
|
||||
/// <param name="pollIntervalSeconds">Number of seconds between polling attempts.</param>
|
||||
public void WaitForState(string className, string keyProperty, string keyValue, Func<ManagementObject, bool> isReached, string description, int timeoutSeconds, int pollIntervalSeconds)
|
||||
{
|
||||
var deadline = DateTime.UtcNow.AddSeconds(Math.Max(1, timeoutSeconds));
|
||||
@@ -163,12 +251,24 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(TimeSpan.FromSeconds(delay));
|
||||
var remaining = deadline - DateTime.UtcNow;
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
break;
|
||||
}
|
||||
Thread.Sleep(remaining < TimeSpan.FromSeconds(delay) ? remaining : TimeSpan.FromSeconds(delay));
|
||||
}
|
||||
|
||||
throw new TimeoutException("Timeout while waiting for " + description + " [" + className + "." + keyProperty + "=" + keyValue + "]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a WMI property as a string without failing on missing or invalid properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object or output parameter object.</param>
|
||||
/// <param name="propertyName">The property to read.</param>
|
||||
/// <param name="fallback">The value returned when the property cannot be read.</param>
|
||||
/// <returns>The property value or the fallback value.</returns>
|
||||
public static string SafeGetString(ManagementBaseObject item, string propertyName, string fallback)
|
||||
{
|
||||
try
|
||||
@@ -187,6 +287,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a WMI property as an integer without failing on missing or invalid properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object or output parameter object.</param>
|
||||
/// <param name="propertyName">The property to read.</param>
|
||||
/// <param name="fallback">The value returned when the property cannot be read.</param>
|
||||
/// <returns>The property value or the fallback value.</returns>
|
||||
public static int SafeGetInt32(ManagementBaseObject item, string propertyName, int fallback)
|
||||
{
|
||||
try
|
||||
@@ -205,6 +312,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a WMI property as a Boolean without failing on missing or invalid properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object or output parameter object.</param>
|
||||
/// <param name="propertyName">The property to read.</param>
|
||||
/// <param name="fallback">The value returned when the property cannot be read.</param>
|
||||
/// <returns>The property value or the fallback value.</returns>
|
||||
public static bool SafeGetBoolean(ManagementBaseObject item, string propertyName, bool fallback)
|
||||
{
|
||||
try
|
||||
@@ -223,6 +337,9 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the management scope is connected before a WMI operation runs.
|
||||
/// </summary>
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (_scope == null || !_scope.IsConnected)
|
||||
@@ -231,6 +348,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a WMI object exposes a property, using case-insensitive comparison.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object to inspect.</param>
|
||||
/// <param name="propertyName">The property name to find.</param>
|
||||
/// <returns>True when the property exists; otherwise false.</returns>
|
||||
private static bool HasProperty(ManagementBaseObject item, string propertyName)
|
||||
{
|
||||
foreach (PropertyData property in item.Properties)
|
||||
@@ -244,6 +367,14 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a WMI object matches a requested key value.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object to inspect.</param>
|
||||
/// <param name="className">The WMI class name of the object.</param>
|
||||
/// <param name="propertyName">The requested key property.</param>
|
||||
/// <param name="expectedValue">The expected key value.</param>
|
||||
/// <returns>True when the object matches the requested value.</returns>
|
||||
private static bool MatchesProperty(ManagementBaseObject item, string className, string propertyName, string expectedValue)
|
||||
{
|
||||
foreach (var candidate in CandidatePropertyNames(className, propertyName))
|
||||
@@ -263,6 +394,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the primary and compatibility property names for a WMI lookup.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name being queried.</param>
|
||||
/// <param name="propertyName">The requested key property.</param>
|
||||
/// <returns>The candidate property names to inspect.</returns>
|
||||
private static IEnumerable<string> CandidatePropertyNames(string className, string propertyName)
|
||||
{
|
||||
yield return propertyName;
|
||||
@@ -274,6 +411,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a readable object name for diagnostics without letting WMI metadata failures escape.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WMI object being described.</param>
|
||||
/// <returns>A relative WMI path or fallback object name.</returns>
|
||||
private static string SafeObjectName(ManagementObject instance)
|
||||
{
|
||||
try
|
||||
@@ -286,6 +428,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the exact method casing exposed by the WMI class.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WMI object whose class should be inspected.</param>
|
||||
/// <param name="requestedName">The requested method name.</param>
|
||||
/// <returns>The method name as exposed by WMI.</returns>
|
||||
private static string ResolveMethodName(ManagementObject instance, string requestedName)
|
||||
{
|
||||
using (var managementClass = new ManagementClass(instance.Scope, new ManagementPath(instance.Path.ClassName), null))
|
||||
@@ -302,6 +450,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
throw new MissingMethodException(instance.Path.ClassName, requestedName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the WMI return code from either a scalar return value or output parameters.
|
||||
/// </summary>
|
||||
/// <param name="result">The object returned by ManagementObject.InvokeMethod.</param>
|
||||
/// <returns>The numeric WMI return code.</returns>
|
||||
private static uint ExtractReturnCode(object result)
|
||||
{
|
||||
if (result == null)
|
||||
@@ -318,8 +471,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return Convert.ToUInt32(result, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases resources owned by this client.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_scope = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,16 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes snapshot and diff data to CSV files for review outside the GUI.
|
||||
/// </summary>
|
||||
public static class CsvWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes application artifact states from a snapshot to a CSV file.
|
||||
/// </summary>
|
||||
/// <param name="path">The target CSV file path.</param>
|
||||
/// <param name="snapshot">The snapshot whose artifact states should be exported.</param>
|
||||
public static void WriteSnapshotArtifacts(string path, BizTalkSnapshot snapshot)
|
||||
{
|
||||
var lines = new List<string> { "Application,Type,Name,Status" };
|
||||
@@ -28,6 +36,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes host instance states from a snapshot to a CSV file.
|
||||
/// </summary>
|
||||
/// <param name="path">The target CSV file path.</param>
|
||||
/// <param name="snapshot">The snapshot whose host instance states should be exported.</param>
|
||||
public static void WriteSnapshotHosts(string path, BizTalkSnapshot snapshot)
|
||||
{
|
||||
var lines = new List<string> { "InstanceName,HostName,Server,State" };
|
||||
@@ -38,6 +51,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes snapshot differences to a CSV file.
|
||||
/// </summary>
|
||||
/// <param name="path">The target CSV file path.</param>
|
||||
/// <param name="diff">The diff model to export.</param>
|
||||
public static void WriteDiff(string path, SnapshotDiff diff)
|
||||
{
|
||||
var lines = new List<string> { "Scope,Application,Type,Name,Server,Before,After" };
|
||||
@@ -52,6 +70,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds one CSV row from already ordered field values.
|
||||
/// </summary>
|
||||
/// <param name="values">The values that should be escaped and joined.</param>
|
||||
/// <returns>A single CSV row.</returns>
|
||||
private static string Row(params string[] values)
|
||||
{
|
||||
var escaped = new string[values.Length];
|
||||
@@ -62,9 +85,18 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return string.Join(",", escaped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a CSV field when it contains separators, quotes or line breaks.
|
||||
/// </summary>
|
||||
/// <param name="value">The raw field value.</param>
|
||||
/// <returns>The CSV-safe field value.</returns>
|
||||
private static string Escape(string value)
|
||||
{
|
||||
value = value ?? string.Empty;
|
||||
if (value.Length > 0 && (value[0] == '=' || value[0] == '+' || value[0] == '-' || value[0] == '@' || value[0] == '\t'))
|
||||
{
|
||||
value = "'" + value;
|
||||
}
|
||||
if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0)
|
||||
{
|
||||
return value;
|
||||
|
||||
@@ -4,8 +4,16 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes HTML reports for BizTalk snapshots and snapshot differences.
|
||||
/// </summary>
|
||||
public static class HtmlReportWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a complete HTML snapshot report.
|
||||
/// </summary>
|
||||
/// <param name="path">The target HTML file path.</param>
|
||||
/// <param name="snapshot">The snapshot to render.</param>
|
||||
public static void WriteSnapshot(string path, BizTalkSnapshot snapshot)
|
||||
{
|
||||
var html = new StringBuilder();
|
||||
@@ -50,6 +58,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a complete HTML diff report.
|
||||
/// </summary>
|
||||
/// <param name="path">The target HTML file path.</param>
|
||||
/// <param name="diff">The diff model to render.</param>
|
||||
public static void WriteDiff(string path, SnapshotDiff diff)
|
||||
{
|
||||
var html = new StringBuilder();
|
||||
@@ -74,12 +87,26 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one artifact status row to a report table.
|
||||
/// </summary>
|
||||
/// <param name="html">The report builder receiving the row markup.</param>
|
||||
/// <param name="name">The artifact name.</param>
|
||||
/// <param name="status">The formatted artifact status.</param>
|
||||
/// <param name="ok">True when the row should use the positive status style.</param>
|
||||
/// <param name="detail1">The first detail column value.</param>
|
||||
/// <param name="detail2">The second detail column value.</param>
|
||||
private static void StatusRow(StringBuilder html, string name, string status, bool ok, string detail1, string detail2)
|
||||
{
|
||||
html.Append("<tr><td>").Append(Encode(name)).Append("</td><td class='").Append(ok ? "ok" : "bad").Append("'>").Append(Encode(status))
|
||||
.Append("</td><td>").Append(Encode(detail1)).Append("</td><td>").Append(Encode(detail2)).Append("</td></tr>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the common document header, style block and title.
|
||||
/// </summary>
|
||||
/// <param name="html">The report builder receiving the header markup.</param>
|
||||
/// <param name="title">The document title and main heading.</param>
|
||||
private static void Header(StringBuilder html, string title)
|
||||
{
|
||||
html.Append("<!doctype html><html><head><meta charset='utf-8'><title>").Append(Encode(title)).Append("</title><style>")
|
||||
@@ -87,11 +114,20 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
.Append("</style></head><body><h1>").Append(Encode(title)).Append("</h1>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the common HTML document footer.
|
||||
/// </summary>
|
||||
/// <param name="html">The report builder receiving the footer markup.</param>
|
||||
private static void Footer(StringBuilder html)
|
||||
{
|
||||
html.Append("</body></html>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTML-encodes report values and treats null values as empty text.
|
||||
/// </summary>
|
||||
/// <param name="value">The raw value to encode.</param>
|
||||
/// <returns>An HTML-safe value.</returns>
|
||||
private static string Encode(string value)
|
||||
{
|
||||
return WebUtility.HtmlEncode(value ?? string.Empty);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Json;
|
||||
@@ -5,13 +6,32 @@ using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Persists DataContract models as JSON with repository-defined encoding rules.
|
||||
/// </summary>
|
||||
public static class JsonFileStore
|
||||
{
|
||||
/// <summary>
|
||||
/// UTF-8 encoding instance that writes JSON without a byte order mark.
|
||||
/// </summary>
|
||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a value to a UTF-8 JSON file without a byte order mark.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The model type to serialize.</typeparam>
|
||||
/// <param name="path">The target JSON file path.</param>
|
||||
/// <param name="value">The value to serialize.</param>
|
||||
public static void Save<T>(string path, T value)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("A JSON target path is required.", "path");
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
Directory.CreateDirectory(directory);
|
||||
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
||||
{
|
||||
UseSimpleDictionaryFormat = true
|
||||
@@ -21,10 +41,16 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
serializer.WriteObject(stream, value);
|
||||
var json = Utf8NoBom.GetString(stream.ToArray());
|
||||
File.WriteAllText(path, json, Utf8NoBom);
|
||||
WriteAtomically(fullPath, json);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a JSON file into the requested DataContract model type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The model type to deserialize.</typeparam>
|
||||
/// <param name="path">The JSON file path to load.</param>
|
||||
/// <returns>The deserialized model.</returns>
|
||||
public static T Load<T>(string path)
|
||||
{
|
||||
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
||||
@@ -46,6 +72,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes byte order mark variants that may exist in previously written files.
|
||||
/// </summary>
|
||||
/// <param name="json">The raw JSON text read from disk.</param>
|
||||
/// <returns>The JSON text without a leading BOM marker.</returns>
|
||||
private static string NormalizeJson(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
@@ -66,6 +97,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a short diagnostic message for a JSON deserialization failure.
|
||||
/// </summary>
|
||||
/// <param name="json">The normalized JSON text that failed to deserialize.</param>
|
||||
/// <returns>A diagnostic suffix describing the beginning of the JSON content.</returns>
|
||||
private static string DescribeJsonStart(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
@@ -82,5 +118,75 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
return "The file starts with valid JSON syntax but could not be deserialized into the expected model.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a file through a same-directory temporary file so an interrupted
|
||||
/// save cannot leave a truncated snapshot or operation plan behind.
|
||||
/// </summary>
|
||||
private static void WriteAtomically(string path, string content)
|
||||
{
|
||||
var temporaryPath = path + ".tmp." + Guid.NewGuid().ToString("N");
|
||||
var backupPath = path + ".bak." + Guid.NewGuid().ToString("N");
|
||||
try
|
||||
{
|
||||
File.WriteAllText(temporaryPath, content, Utf8NoBom);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
File.Move(temporaryPath, path);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.Replace(temporaryPath, path, backupPath, true);
|
||||
TryDelete(backupPath);
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
ReplaceWithRenameFallback(path, temporaryPath, backupPath);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
ReplaceWithRenameFallback(path, temporaryPath, backupPath);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(temporaryPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReplaceWithRenameFallback(string path, string temporaryPath, string backupPath)
|
||||
{
|
||||
File.Move(path, backupPath);
|
||||
try
|
||||
{
|
||||
File.Move(temporaryPath, path);
|
||||
TryDelete(backupPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!File.Exists(path) && File.Exists(backupPath))
|
||||
{
|
||||
File.Move(backupPath, path);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Temporary cleanup is best-effort and must not hide the save result.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,39 +5,107 @@ using System.Threading;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the severity used for visible and file-based operation log entries.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Informational progress or diagnostic entry.
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// Non-fatal condition that needs operator attention.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Failed operation or exception entry.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// Successful operation entry.
|
||||
/// </summary>
|
||||
Success
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one operation log entry displayed in the GUI and written to disk.
|
||||
/// </summary>
|
||||
public sealed class LogEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the local time when the entry was created.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entry severity.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operator-facing message.
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes operation log entries to a daily rolling file and an optional UI sink.
|
||||
/// </summary>
|
||||
public sealed class OperationLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Prefix used for daily log files written to the resolved log directory.
|
||||
/// </summary>
|
||||
private const string LogFilePrefix = "BizTalkPlatformManagementTool-";
|
||||
|
||||
/// <summary>
|
||||
/// File extension used for operation log files.
|
||||
/// </summary>
|
||||
private const string LogFileExtension = ".log";
|
||||
|
||||
/// <summary>
|
||||
/// Number of daily log files retained, including the current day.
|
||||
/// </summary>
|
||||
private const int RetentionDays = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide lock that serializes log file appends.
|
||||
/// </summary>
|
||||
private static readonly object FileLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide flag that ensures log cleanup runs only once.
|
||||
/// </summary>
|
||||
private static int _cleanupDone;
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback for forwarding entries to the UI.
|
||||
/// </summary>
|
||||
private readonly Action<LogEntry> _sink;
|
||||
|
||||
/// <summary>
|
||||
/// Directory where daily log files are written.
|
||||
/// </summary>
|
||||
private readonly string _logDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new logger that writes beside the executable.
|
||||
/// </summary>
|
||||
/// <param name="sink">Optional callback that receives entries for display.</param>
|
||||
public OperationLogger(Action<LogEntry> sink)
|
||||
{
|
||||
_sink = sink;
|
||||
_logDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
_logDirectory = ResolveLogDirectory();
|
||||
CleanupOldLogs();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the daily log file for the current date.
|
||||
/// </summary>
|
||||
public string LogFilePath
|
||||
{
|
||||
get
|
||||
@@ -46,26 +114,47 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an informational entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Info(string message)
|
||||
{
|
||||
Write(LogLevel.Info, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a warning entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Warning(string message)
|
||||
{
|
||||
Write(LogLevel.Warning, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an error entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Error(string message)
|
||||
{
|
||||
Write(LogLevel.Error, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a success entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Success(string message)
|
||||
{
|
||||
Write(LogLevel.Success, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a log entry and sends it to both output targets.
|
||||
/// </summary>
|
||||
/// <param name="level">The entry severity.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
private void Write(LogLevel level, string message)
|
||||
{
|
||||
var entry = new LogEntry
|
||||
@@ -85,6 +174,10 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
_sink(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one entry to the current daily log file.
|
||||
/// </summary>
|
||||
/// <param name="entry">The entry to write.</param>
|
||||
private void WriteToFile(LogEntry entry)
|
||||
{
|
||||
try
|
||||
@@ -108,6 +201,9 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes log files older than the configured retention window.
|
||||
/// </summary>
|
||||
private void CleanupOldLogs()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _cleanupDone, 1) == 1)
|
||||
@@ -132,5 +228,20 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
// Log retention cleanup is best-effort.
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveLogDirectory()
|
||||
{
|
||||
var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
var preferred = Path.Combine(commonData, "BizTalkPlatformManagementTool", "Logs");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(preferred);
|
||||
return preferred;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return AppDomain.CurrentDomain.BaseDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,26 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares two BizTalk snapshots and returns the state differences that matter
|
||||
/// after a maintenance window.
|
||||
/// </summary>
|
||||
public static class SnapshotComparer
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares all supported artifact and host instance states.
|
||||
/// </summary>
|
||||
/// <param name="before">The snapshot captured before maintenance.</param>
|
||||
/// <param name="after">The snapshot captured after maintenance.</param>
|
||||
/// <returns>A diff containing changed, new and missing artifacts.</returns>
|
||||
public static SnapshotDiff Compare(BizTalkSnapshot before, BizTalkSnapshot after)
|
||||
{
|
||||
SnapshotValidator.Validate(before);
|
||||
SnapshotValidator.Validate(after);
|
||||
if (!SnapshotValidator.ServerNamesEqual(before.Server, after.Server))
|
||||
{
|
||||
throw new InvalidOperationException("Snapshots from different servers cannot be compared: '" + before.Server + "' and '" + after.Server + "'.");
|
||||
}
|
||||
var diff = new SnapshotDiff();
|
||||
|
||||
CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after));
|
||||
@@ -18,6 +34,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return diff;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds receive location differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Receive locations keyed by name from the before snapshot.</param>
|
||||
/// <param name="after">Receive locations keyed by name from the after snapshot.</param>
|
||||
private static void CompareReceiveLocations(SnapshotDiff diff, Dictionary<string, ReceiveLocationState> before, Dictionary<string, ReceiveLocationState> after)
|
||||
{
|
||||
foreach (var pair in after)
|
||||
@@ -42,6 +64,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds send port differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Send ports keyed by name from the before snapshot.</param>
|
||||
/// <param name="after">Send ports keyed by name from the after snapshot.</param>
|
||||
private static void CompareSendPorts(SnapshotDiff diff, Dictionary<string, SendPortState> before, Dictionary<string, SendPortState> after)
|
||||
{
|
||||
foreach (var pair in after)
|
||||
@@ -66,6 +94,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds orchestration differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Orchestrations keyed by name from the before snapshot.</param>
|
||||
/// <param name="after">Orchestrations keyed by name from the after snapshot.</param>
|
||||
private static void CompareOrchestrations(SnapshotDiff diff, Dictionary<string, OrchestrationState> before, Dictionary<string, OrchestrationState> after)
|
||||
{
|
||||
foreach (var pair in after)
|
||||
@@ -90,6 +124,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds host instance differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Host instances from the before snapshot.</param>
|
||||
/// <param name="after">Host instances from the after snapshot.</param>
|
||||
private static void CompareHostInstances(SnapshotDiff diff, List<HostInstanceState> before, List<HostInstanceState> after)
|
||||
{
|
||||
var beforeMap = new Dictionary<string, HostInstanceState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -97,11 +137,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
foreach (var item in before)
|
||||
{
|
||||
beforeMap[item.InstanceName ?? string.Empty] = item;
|
||||
beforeMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item;
|
||||
}
|
||||
foreach (var item in after)
|
||||
{
|
||||
afterMap[item.InstanceName ?? string.Empty] = item;
|
||||
afterMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item;
|
||||
}
|
||||
|
||||
foreach (var pair in afterMap)
|
||||
@@ -126,6 +166,15 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one artifact diff entry.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entry.</param>
|
||||
/// <param name="application">The BizTalk application name.</param>
|
||||
/// <param name="type">The artifact type.</param>
|
||||
/// <param name="name">The artifact name.</param>
|
||||
/// <param name="before">The formatted before state.</param>
|
||||
/// <param name="after">The formatted after state.</param>
|
||||
private static void AddArtifact(SnapshotDiff diff, string application, string type, string name, string before, string after)
|
||||
{
|
||||
diff.ArtifactDifferences.Add(new ArtifactDiffEntry
|
||||
@@ -138,6 +187,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens receive locations from all applications into a case-insensitive name map.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to flatten.</param>
|
||||
/// <returns>A map keyed by receive location name.</returns>
|
||||
private static Dictionary<string, ReceiveLocationState> FlattenReceiveLocations(BizTalkSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, ReceiveLocationState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -145,12 +199,17 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.ReceiveLocations)
|
||||
{
|
||||
map[item.Name ?? string.Empty] = item;
|
||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens send ports from all applications into a case-insensitive name map.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to flatten.</param>
|
||||
/// <returns>A map keyed by send port name.</returns>
|
||||
private static Dictionary<string, SendPortState> FlattenSendPorts(BizTalkSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, SendPortState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -158,12 +217,17 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.SendPorts)
|
||||
{
|
||||
map[item.Name ?? string.Empty] = item;
|
||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens orchestrations from all applications into a case-insensitive name map.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to flatten.</param>
|
||||
/// <returns>A map keyed by orchestration name.</returns>
|
||||
private static Dictionary<string, OrchestrationState> FlattenOrchestrations(BizTalkSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, OrchestrationState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -171,7 +235,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.Orchestrations)
|
||||
{
|
||||
map[item.Name ?? string.Empty] = item;
|
||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
|
||||
@@ -3,8 +3,16 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves snapshot and diff models together with their sidecar report formats.
|
||||
/// </summary>
|
||||
public static class SnapshotStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a snapshot as JSON and creates CSV, host CSV and HTML sidecars.
|
||||
/// </summary>
|
||||
/// <param name="jsonPath">The primary JSON output path.</param>
|
||||
/// <param name="snapshot">The snapshot to persist.</param>
|
||||
public static void SaveSnapshotSet(string jsonPath, BizTalkSnapshot snapshot)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
||||
@@ -14,6 +22,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
HtmlReportWriter.WriteSnapshot(jsonPath + ".html", snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a diff as JSON and creates CSV and HTML sidecars.
|
||||
/// </summary>
|
||||
/// <param name="jsonPath">The primary JSON output path.</param>
|
||||
/// <param name="diff">The diff model to persist.</param>
|
||||
public static void SaveDiffSet(string jsonPath, SnapshotDiff diff)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes deserialized legacy snapshots and rejects ambiguous or unsafe input.
|
||||
/// </summary>
|
||||
public static class SnapshotValidator
|
||||
{
|
||||
/// <summary>Normalizes optional collections and rejects missing or duplicate artifact identities.</summary>
|
||||
public static void Validate(BizTalkSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null)
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot is empty.");
|
||||
}
|
||||
|
||||
snapshot.Applications = snapshot.Applications ?? new List<ApplicationSnapshot>();
|
||||
snapshot.HostInstances = snapshot.HostInstances ?? new List<HostInstanceState>();
|
||||
|
||||
var receiveLocations = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var sendPorts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var orchestrations = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var app in snapshot.Applications)
|
||||
{
|
||||
if (app == null || string.IsNullOrWhiteSpace(app.Application))
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot contains an application without a name.");
|
||||
}
|
||||
|
||||
app.ReceiveLocations = app.ReceiveLocations ?? new List<ReceiveLocationState>();
|
||||
app.SendPorts = app.SendPorts ?? new List<SendPortState>();
|
||||
app.Orchestrations = app.Orchestrations ?? new List<OrchestrationState>();
|
||||
|
||||
ValidateArtifacts(app.Application, "receive location", app.ReceiveLocations, x => x == null ? null : x.Name, receiveLocations);
|
||||
ValidateArtifacts(app.Application, "send port", app.SendPorts, x => x == null ? null : x.Name, sendPorts);
|
||||
ValidateArtifacts(app.Application, "orchestration", app.Orchestrations, x => x == null ? null : x.Name, orchestrations);
|
||||
}
|
||||
|
||||
var hostInstances = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var host in snapshot.HostInstances)
|
||||
{
|
||||
if (host == null || string.IsNullOrWhiteSpace(host.InstanceName))
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot contains a host instance without an instance name.");
|
||||
}
|
||||
if (!hostInstances.Add(host.InstanceName))
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot contains the host instance more than once: " + host.InstanceName);
|
||||
}
|
||||
host.StateText = ArtifactStates.FormatHostInstance(host.RawState);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Validates a snapshot and ensures it belongs to the requested operation server.</summary>
|
||||
public static void EnsureServerMatches(BizTalkSnapshot snapshot, string targetServer)
|
||||
{
|
||||
Validate(snapshot);
|
||||
if (string.IsNullOrWhiteSpace(snapshot.Server) || string.IsNullOrWhiteSpace(targetServer))
|
||||
{
|
||||
throw new InvalidOperationException("Snapshot server and target server must both be specified before a restore plan can be created.");
|
||||
}
|
||||
if (!ServerNamesEqual(snapshot.Server, targetServer))
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot belongs to server '" + snapshot.Server + "' but the selected restore target is '" + targetServer + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Compares server names while accepting short-name/FQDN variants of the same host.</summary>
|
||||
public static bool ServerNamesEqual(string left, string right)
|
||||
{
|
||||
var normalizedLeft = NormalizeServer(left);
|
||||
var normalizedRight = NormalizeServer(right);
|
||||
if (string.Equals(normalizedLeft, normalizedRight, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return string.Equals(ShortName(normalizedLeft), ShortName(normalizedRight), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Builds the collision-safe identity used for application artifacts.</summary>
|
||||
public static string ArtifactKey(string application, string name)
|
||||
{
|
||||
return (application ?? string.Empty).Trim() + "\u001f" + (name ?? string.Empty).Trim();
|
||||
}
|
||||
|
||||
private static void ValidateArtifacts<T>(string application, string type, IEnumerable<T> values, Func<T, string> getName, HashSet<string> keys)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var name = getName(value);
|
||||
if (value == null || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new InvalidOperationException("Application '" + application + "' contains a " + type + " without a name.");
|
||||
}
|
||||
var key = ArtifactKey(application, name);
|
||||
if (!keys.Add(key))
|
||||
{
|
||||
throw new InvalidOperationException("Application '" + application + "' contains the " + type + " more than once: " + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeServer(string value)
|
||||
{
|
||||
value = (value ?? string.Empty).Trim().TrimStart('\\');
|
||||
if (value == "." || string.Equals(value, "localhost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Environment.MachineName;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string ShortName(string value)
|
||||
{
|
||||
var index = value.IndexOf('.');
|
||||
return index < 0 ? value : value.Substring(0, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,31 +9,125 @@ using BizTalkPlatformManagementTool.Services;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Ui
|
||||
{
|
||||
/// <summary>
|
||||
/// Main WinForms surface for diagnosing, snapshotting, comparing, shutting down
|
||||
/// and restoring a BizTalk platform state.
|
||||
/// </summary>
|
||||
public sealed class MainForm : Form
|
||||
{
|
||||
/// <summary>
|
||||
/// Text input for the BizTalk server or management host.
|
||||
/// </summary>
|
||||
private readonly TextBox _serverTextBox = new TextBox();
|
||||
|
||||
/// <summary>
|
||||
/// Text input for the directory that receives snapshots, plans and reports.
|
||||
/// </summary>
|
||||
private readonly TextBox _outputTextBox = new TextBox();
|
||||
|
||||
/// <summary>
|
||||
/// Text input for the restore state file name or path.
|
||||
/// </summary>
|
||||
private readonly TextBox _stateFileTextBox = new TextBox();
|
||||
|
||||
/// <summary>
|
||||
/// Numeric input for the maximum wait time of runtime state changes.
|
||||
/// </summary>
|
||||
private readonly NumericUpDown _timeoutInput = new NumericUpDown();
|
||||
|
||||
/// <summary>
|
||||
/// Numeric input for the WMI polling interval.
|
||||
/// </summary>
|
||||
private readonly NumericUpDown _pollInput = new NumericUpDown();
|
||||
|
||||
/// <summary>
|
||||
/// Checkbox that keeps shutdown and restore operations in dry-run mode.
|
||||
/// </summary>
|
||||
private readonly CheckBox _dryRunCheckBox = new CheckBox();
|
||||
|
||||
/// <summary>
|
||||
/// Grid used to show snapshots, diffs and operation plans.
|
||||
/// </summary>
|
||||
private readonly DataGridView _statusGrid = new DataGridView();
|
||||
|
||||
/// <summary>
|
||||
/// Grid used to show operation log entries.
|
||||
/// </summary>
|
||||
private readonly DataGridView _logGrid = new DataGridView();
|
||||
|
||||
/// <summary>
|
||||
/// Status strip at the bottom of the form.
|
||||
/// </summary>
|
||||
private readonly StatusStrip _statusStrip = new StatusStrip();
|
||||
|
||||
/// <summary>
|
||||
/// Text label inside the status strip.
|
||||
/// </summary>
|
||||
private readonly ToolStripStatusLabel _statusLabel = new ToolStripStatusLabel();
|
||||
|
||||
/// <summary>
|
||||
/// Header indicator derived from the latest host instance snapshot.
|
||||
/// </summary>
|
||||
private readonly Label _environmentStatusLabel = new Label();
|
||||
|
||||
/// <summary>
|
||||
/// Logger that writes to disk and mirrors entries into the UI log grid.
|
||||
/// </summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Service that performs all BizTalk runtime operations.
|
||||
/// </summary>
|
||||
private readonly BizTalkOperationService _service;
|
||||
|
||||
/// <summary>
|
||||
/// Button that validates WMI access.
|
||||
/// </summary>
|
||||
private Button _diagnoseButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that creates the before snapshot.
|
||||
/// </summary>
|
||||
private Button _beforeButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that creates the after snapshot.
|
||||
/// </summary>
|
||||
private Button _afterButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that compares before and after snapshots.
|
||||
/// </summary>
|
||||
private Button _compareButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that creates and executes the shutdown plan.
|
||||
/// </summary>
|
||||
private Button _shutdownButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that creates and executes the restore plan.
|
||||
/// </summary>
|
||||
private Button _restoreButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that clears visible grids and status.
|
||||
/// </summary>
|
||||
private Button _clearButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that closes the application.
|
||||
/// </summary>
|
||||
private Button _closeButton;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a background operation is still active.
|
||||
/// </summary>
|
||||
private bool _isBusy;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the form, operation services and visual controls.
|
||||
/// </summary>
|
||||
public MainForm()
|
||||
{
|
||||
Text = "BizTalk Platform Management Tool";
|
||||
@@ -45,9 +139,13 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
_logger = new OperationLogger(AppendLog);
|
||||
_service = new BizTalkOperationService(_logger);
|
||||
BuildUi();
|
||||
FormClosing += MainFormClosing;
|
||||
_logger.Info("Log file: " + _logger.LogFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the root form layout and adds settings, action, result and status areas.
|
||||
/// </summary>
|
||||
private void BuildUi()
|
||||
{
|
||||
var root = new TableLayoutPanel
|
||||
@@ -72,6 +170,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
root.Controls.Add(_statusStrip, 0, 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the settings panel with server, output, state file and timing inputs.
|
||||
/// </summary>
|
||||
/// <returns>The configured settings panel.</returns>
|
||||
private Control BuildSettingsPanel()
|
||||
{
|
||||
var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 10, RowCount = 2 };
|
||||
@@ -140,6 +242,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
return panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the toolbar-like action panel.
|
||||
/// </summary>
|
||||
/// <returns>The configured action panel.</returns>
|
||||
private Control BuildActionPanel()
|
||||
{
|
||||
var panel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, Padding = new Padding(0, 8, 0, 0), WrapContents = false };
|
||||
@@ -163,6 +269,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
return panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the tab control containing status/results and operation logs.
|
||||
/// </summary>
|
||||
/// <returns>The configured tab control.</returns>
|
||||
private Control BuildTabs()
|
||||
{
|
||||
var tabs = new TabControl { Dock = DockStyle.Fill };
|
||||
@@ -189,12 +299,22 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
return tabs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Diagnose button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void DiagnoseClick(object sender, EventArgs e)
|
||||
{
|
||||
var server = _serverTextBox.Text.Trim();
|
||||
RunAsync("Diagnosing WMI access...", () => _service.Diagnose(server));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Snapshot Before button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void BeforeClick(object sender, EventArgs e)
|
||||
{
|
||||
var options = GetOptions();
|
||||
@@ -206,6 +326,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Snapshot After button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void AfterClick(object sender, EventArgs e)
|
||||
{
|
||||
var options = GetOptions();
|
||||
@@ -217,6 +342,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Compare button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void CompareClick(object sender, EventArgs e)
|
||||
{
|
||||
var options = GetOptions();
|
||||
@@ -230,21 +360,26 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Shutdown button click and runs the guarded shutdown workflow.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
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);
|
||||
var planPath = _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan);
|
||||
ShowPlan(plan);
|
||||
if (!options.DryRun && !ConfirmPreparedPlan("Shutdown", plan, options.Server, planPath))
|
||||
{
|
||||
_logger.Warning("Shutdown cancelled after plan review. No runtime state was changed.");
|
||||
return;
|
||||
}
|
||||
_service.ExecutePlan(plan, options);
|
||||
if (!options.DryRun)
|
||||
{
|
||||
@@ -255,20 +390,25 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Restore button click and runs the guarded restore workflow.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void RestoreClick(object sender, EventArgs e)
|
||||
{
|
||||
if (!ConfirmDangerousAction("Restore"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var options = GetOptions();
|
||||
RunAsync("Preparing restore...", () =>
|
||||
{
|
||||
var snapshot = JsonFileStore.Load<BizTalkSnapshot>(ResolveStateFile(options));
|
||||
var plan = _service.CreateRestorePlan(snapshot, options.Server);
|
||||
_service.SavePlan(options.OutputDirectory, "restore-plan.json", plan);
|
||||
var planPath = _service.SavePlan(options.OutputDirectory, "restore-plan.json", plan);
|
||||
ShowPlan(plan);
|
||||
if (!options.DryRun && !ConfirmPreparedPlan("Restore", plan, options.Server, planPath))
|
||||
{
|
||||
_logger.Warning("Restore cancelled after plan review. No runtime state was changed.");
|
||||
return;
|
||||
}
|
||||
_service.ExecutePlan(plan, options);
|
||||
if (!options.DryRun)
|
||||
{
|
||||
@@ -279,6 +419,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Clear button click by removing visible state without deleting files.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void ClearClick(object sender, EventArgs e)
|
||||
{
|
||||
_statusGrid.Rows.Clear();
|
||||
@@ -287,11 +432,21 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
_statusLabel.Text = "Ready.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Close button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void CloseClick(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs long-running BizTalk work on a background task and keeps the UI responsive.
|
||||
/// </summary>
|
||||
/// <param name="status">The status text shown while the work is running.</param>
|
||||
/// <param name="work">The work to execute on the background task.</param>
|
||||
private void RunAsync(string status, Action work)
|
||||
{
|
||||
SetBusy(true, status);
|
||||
@@ -311,6 +466,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads and normalizes the current UI options.
|
||||
/// </summary>
|
||||
/// <returns>The runtime options selected by the user.</returns>
|
||||
private OperationOptions GetOptions()
|
||||
{
|
||||
var server = string.IsNullOrWhiteSpace(_serverTextBox.Text) ? Environment.MachineName : _serverTextBox.Text.Trim();
|
||||
@@ -330,27 +489,52 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a restore state file against the output directory when it is relative.
|
||||
/// </summary>
|
||||
/// <param name="options">The options containing the state file and output directory.</param>
|
||||
/// <returns>The absolute or output-relative state file path.</returns>
|
||||
private string ResolveStateFile(OperationOptions options)
|
||||
{
|
||||
return Path.IsPathRooted(options.StateFile) ? options.StateFile : Path.Combine(options.OutputDirectory, options.StateFile);
|
||||
}
|
||||
|
||||
private bool ConfirmDangerousAction(string actionName)
|
||||
/// <summary>
|
||||
/// Confirms a fully prepared runtime-changing plan immediately before execution.
|
||||
/// </summary>
|
||||
/// <param name="actionName">The action name displayed in the confirmation dialog.</param>
|
||||
/// <returns>True when the action may continue; otherwise false.</returns>
|
||||
private bool ConfirmPreparedPlan(string actionName, OperationPlan plan, string server, string planPath)
|
||||
{
|
||||
if (_dryRunCheckBox.Checked)
|
||||
var confirmed = false;
|
||||
Action showConfirmation = () =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var executableSteps = plan.Steps.Count(x => x.Execute);
|
||||
var result = MessageBox.Show(
|
||||
actionName + " will execute " + executableSteps + " step(s) on server '" + server + "'.\n\n"
|
||||
+ "The exact plan was saved to:\n" + planPath + "\n\nContinue now?",
|
||||
"Confirm Prepared BizTalk Plan",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Warning,
|
||||
MessageBoxDefaultButton.Button2);
|
||||
confirmed = result == DialogResult.Yes;
|
||||
};
|
||||
|
||||
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;
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(showConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
showConfirmation();
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a snapshot in the status grid and updates the environment indicator.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to display.</param>
|
||||
private void ShowSnapshot(BizTalkSnapshot snapshot)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
@@ -379,6 +563,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays snapshot differences in the status grid.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff to display.</param>
|
||||
private void ShowDiff(SnapshotDiff diff)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
@@ -395,6 +583,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays an operation plan in the status grid before or during execution.
|
||||
/// </summary>
|
||||
/// <param name="plan">The plan to display.</param>
|
||||
private void ShowPlan(OperationPlan plan)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
@@ -407,6 +599,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one operation log entry to the log grid.
|
||||
/// </summary>
|
||||
/// <param name="entry">The log entry to display.</param>
|
||||
private void AppendLog(LogEntry entry)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
@@ -429,10 +625,16 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables action buttons and updates the status strip.
|
||||
/// </summary>
|
||||
/// <param name="busy">True while a background operation is running.</param>
|
||||
/// <param name="status">The status text to display.</param>
|
||||
private void SetBusy(bool busy, string status)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
{
|
||||
_isBusy = busy;
|
||||
_diagnoseButton.Enabled = !busy;
|
||||
_beforeButton.Enabled = !busy;
|
||||
_afterButton.Enabled = !busy;
|
||||
@@ -445,6 +647,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the output directory Browse button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void BrowseButtonClick(object sender, EventArgs e)
|
||||
{
|
||||
using (var dialog = new FolderBrowserDialog())
|
||||
@@ -457,16 +664,27 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a UI update on the UI thread when required.
|
||||
/// </summary>
|
||||
/// <param name="action">The UI action to execute.</param>
|
||||
private void InvokeIfRequired(Action action)
|
||||
{
|
||||
if (IsDisposed)
|
||||
if (IsDisposed || Disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (InvokeRequired)
|
||||
{
|
||||
BeginInvoke(action);
|
||||
try
|
||||
{
|
||||
BeginInvoke(action);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// The form was closed between the state check and BeginInvoke.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -474,11 +692,41 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents the form from being disposed while a maintenance operation is active.
|
||||
/// </summary>
|
||||
private void MainFormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (!_isBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"A BizTalk operation is still running. Wait for it to finish before closing the tool.",
|
||||
"Operation in progress",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a right-aligned label for form inputs.
|
||||
/// </summary>
|
||||
/// <param name="text">The label text.</param>
|
||||
/// <returns>The configured label control.</returns>
|
||||
private static Label Label(string text)
|
||||
{
|
||||
return new Label { Text = text, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleRight, Margin = new Padding(0, 5, 4, 5) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a standard action button and attaches its click handler.
|
||||
/// </summary>
|
||||
/// <param name="text">The button text.</param>
|
||||
/// <param name="handler">The click event handler.</param>
|
||||
/// <returns>The configured button.</returns>
|
||||
private static Button ActionButton(string text, EventHandler handler)
|
||||
{
|
||||
var button = new Button { Text = text, Width = 112, Height = 34, Margin = new Padding(0, 0, 8, 0) };
|
||||
@@ -486,12 +734,20 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies common docking and spacing to an input control.
|
||||
/// </summary>
|
||||
/// <param name="control">The control to configure.</param>
|
||||
private static void ConfigureInput(Control control)
|
||||
{
|
||||
control.Dock = DockStyle.Fill;
|
||||
control.Margin = new Padding(4, 6, 8, 6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the environment indicator from host instance states in the latest snapshot.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The latest snapshot, or null when the state is unknown.</param>
|
||||
private void UpdateEnvironmentStatus(BizTalkSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null || snapshot.HostInstances == null || snapshot.HostInstances.Count == 0)
|
||||
@@ -526,6 +782,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies standard read-only display settings to a grid.
|
||||
/// </summary>
|
||||
/// <param name="grid">The grid to configure.</param>
|
||||
private static void ConfigureGrid(DataGridView grid)
|
||||
{
|
||||
grid.Dock = DockStyle.Fill;
|
||||
@@ -538,6 +798,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
grid.BackgroundColor = SystemColors.Window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats an exception chain into a concise message for the operation log.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception to format.</param>
|
||||
/// <returns>A message containing the top-level and unique inner exception messages.</returns>
|
||||
private static string FormatException(Exception ex)
|
||||
{
|
||||
if (ex == null)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="2.1.0.0" name="BizTalkPlatformManagementTool" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user