Support ScheduledTask control and persistent logs
This commit is contained in:
@@ -23,7 +23,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
private const string ProductName = "BizTalk Platform Management Tool";
|
||||
|
||||
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
|
||||
private const string ProductVersion = "2.2.4";
|
||||
private const string ProductVersion = "2.3.0";
|
||||
|
||||
/// <summary>
|
||||
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
AutoSize = true,
|
||||
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
||||
Text = "BizTalk Platform Management Tool 2.2.4"
|
||||
Text = "BizTalk Platform Management Tool 2.3.0"
|
||||
});
|
||||
root.Controls.Add(new Label
|
||||
{
|
||||
|
||||
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
|
||||
[assembly: AssemblyVersion("2.2.4.0")]
|
||||
[assembly: AssemblyFileVersion("2.2.4.0")]
|
||||
[assembly: AssemblyVersion("2.3.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.3.0.0")]
|
||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="2.2.4.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||
<assemblyIdentity version="2.3.0.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
|
||||
</trustInfo>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<!-- Semicolon-delimited optional override. Normal BizTalk 2020 and ScheduledTask 7.x folders are auto-discovered. -->
|
||||
<add key="AdapterAssemblySearchPaths" value="%ProgramFiles(x86)%\Microsoft BizTalk Server 2020;%ProgramFiles(x86)%\BizTalk ScheduledTask Adapter 7.0.2" />
|
||||
</appSettings>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
|
||||
</startup>
|
||||
|
||||
@@ -39,10 +39,12 @@
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
@@ -56,10 +58,12 @@
|
||||
<Compile Include="Models\DiffModels.cs" />
|
||||
<Compile Include="Models\OperationModels.cs" />
|
||||
<Compile Include="Services\BizTalkWmiClient.cs" />
|
||||
<Compile Include="Services\AdapterAssemblyResolver.cs" />
|
||||
<Compile Include="Services\CsvWriter.cs" />
|
||||
<Compile Include="Services\HtmlReportWriter.cs" />
|
||||
<Compile Include="Services\JsonFileStore.cs" />
|
||||
<Compile Include="Services\OperationLogger.cs" />
|
||||
<Compile Include="Services\ExceptionDiagnostics.cs" />
|
||||
<Compile Include="Services\OperationPlanExecutor.cs" />
|
||||
<Compile Include="Services\SnapshotComparer.cs" />
|
||||
<Compile Include="Services\SnapshotValidator.cs" />
|
||||
|
||||
@@ -184,6 +184,14 @@ namespace BizTalkPlatformManagementTool.Models
|
||||
/// </summary>
|
||||
[DataMember(Order = 13)]
|
||||
public string Warning { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the receive adapter name used for dependency preflight.</summary>
|
||||
[DataMember(Order = 14, EmitDefaultValue = false)]
|
||||
public string AdapterName { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the receive transport address used to identify scheduler URIs.</summary>
|
||||
[DataMember(Order = 15, EmitDefaultValue = false)]
|
||||
public string Address { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -9,6 +9,6 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
||||
[assembly: AssemblyVersion("2.2.4.0")]
|
||||
[assembly: AssemblyFileVersion("2.2.4.0")]
|
||||
[assembly: AssemblyVersion("2.3.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.3.0.0")]
|
||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves custom-adapter and BizTalk support assemblies from installed product
|
||||
/// directories for the lifetime of one real operation-plan execution.
|
||||
/// </summary>
|
||||
internal sealed class AdapterAssemblyResolver : IDisposable
|
||||
{
|
||||
/// <summary>The dependency whose absence breaks ScheduledTask Enable/Disable validation.</summary>
|
||||
internal const string SchedulerAssemblyName = "Microsoft.BizTalk.Scheduler";
|
||||
|
||||
/// <summary>Logger receiving all search-path and load diagnostics.</summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>Trusted directories searched after normal CLR resolution has failed.</summary>
|
||||
private readonly List<string> _searchDirectories;
|
||||
|
||||
/// <summary>Prevents duplicate event registration and unregisters safely.</summary>
|
||||
private bool _attached;
|
||||
|
||||
/// <summary>Prevents repeated ScheduledTask dependency preflight in one plan.</summary>
|
||||
private bool _schedulerPrepared;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes and attaches the process-local resolver.
|
||||
/// </summary>
|
||||
/// <param name="logger">The operation logger used for support diagnostics.</param>
|
||||
public AdapterAssemblyResolver(OperationLogger logger)
|
||||
: this(logger, DiscoverSearchDirectories())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a resolver with explicit directories for regression tests.</summary>
|
||||
/// <param name="logger">Optional logger.</param>
|
||||
/// <param name="searchDirectories">Directories that may contain dependencies.</param>
|
||||
internal AdapterAssemblyResolver(OperationLogger logger, IEnumerable<string> searchDirectories)
|
||||
{
|
||||
_logger = logger;
|
||||
_searchDirectories = NormalizeDirectories(searchDirectories).ToList();
|
||||
AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
|
||||
_attached = true;
|
||||
}
|
||||
|
||||
/// <summary>Gets the immutable ordered dependency search path.</summary>
|
||||
internal IList<string> SearchDirectories
|
||||
{
|
||||
get { return _searchDirectories.AsReadOnly(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Preloads the BizTalk Scheduler dependency before WMI validates a Scheduler
|
||||
/// receive location and emits actionable diagnostics when it cannot be found.
|
||||
/// </summary>
|
||||
public void PrepareScheduledTaskAdapter()
|
||||
{
|
||||
if (_schedulerPrepared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_schedulerPrepared = true;
|
||||
|
||||
Info("ScheduledTask adapter preflight. ProcessBitness=" + (Environment.Is64BitProcess ? "64" : "32")
|
||||
+ "; SearchDirectories=" + string.Join(" | ", _searchDirectories.ToArray()));
|
||||
|
||||
var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(x => string.Equals(x.GetName().Name, SchedulerAssemblyName, StringComparison.OrdinalIgnoreCase));
|
||||
if (alreadyLoaded != null)
|
||||
{
|
||||
Success("ScheduledTask dependency already loaded: " + DescribeAssembly(alreadyLoaded));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Normal CLR/GAC resolution remains authoritative. If it fails, the attached
|
||||
// resolver gets the same request and may satisfy it from an installed product path.
|
||||
var normallyResolved = Assembly.Load(new AssemblyName(SchedulerAssemblyName));
|
||||
Success("ScheduledTask dependency resolved through CLR/GAC: " + DescribeAssembly(normallyResolved));
|
||||
return;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// The explicit path diagnostic below reports every searched installation directory.
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
{
|
||||
Warning("ScheduledTask dependency CLR/GAC resolution found an incompatible or unloadable assembly. " + ExceptionDiagnostics.Format(ex));
|
||||
}
|
||||
|
||||
var path = FindCandidateFile(SchedulerAssemblyName, null, _searchDirectories);
|
||||
if (path == null)
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
"ScheduledTask receive-location control requires Microsoft.BizTalk.Scheduler.dll. "
|
||||
+ "The assembly was not found in the BizTalk installation directory, the ScheduledTask Adapter directory, "
|
||||
+ "or configured AdapterAssemblySearchPaths. Search directories: "
|
||||
+ string.Join(" | ", _searchDirectories.ToArray())
|
||||
+ ". Install the matching BizTalk Scheduler assembly in the GAC or add its existing directory to "
|
||||
+ "BizTalkPlatformManagementTool.exe.config; do not copy an assembly from another BizTalk version.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFrom(path);
|
||||
Success("ScheduledTask dependency loaded process-locally: " + DescribeAssembly(assembly));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"ScheduledTask dependency was found but could not be loaded from '" + path + "'. "
|
||||
+ ExceptionDiagnostics.Format(ex), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Unregisters the process-local resolver.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_attached)
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly;
|
||||
_attached = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles unresolved CLR assembly requests using identity-checked files.</summary>
|
||||
private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requested = new AssemblyName(args.Name);
|
||||
var loaded = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => AssemblyIdentityMatches(requested, x.GetName()));
|
||||
if (loaded != null)
|
||||
{
|
||||
return loaded;
|
||||
}
|
||||
|
||||
var path = FindCandidateFile(requested.Name, requested, _searchDirectories);
|
||||
if (path == null)
|
||||
{
|
||||
Warning("Assembly resolution failed. Requested=" + args.Name
|
||||
+ "; RequestingAssembly=" + (args.RequestingAssembly == null ? "(unknown)" : args.RequestingAssembly.FullName)
|
||||
+ "; SearchDirectories=" + string.Join(" | ", _searchDirectories.ToArray()));
|
||||
return null;
|
||||
}
|
||||
|
||||
var assembly = Assembly.LoadFrom(path);
|
||||
Success("Resolved adapter dependency process-locally. Requested=" + args.Name
|
||||
+ "; Loaded=" + DescribeAssembly(assembly));
|
||||
return assembly;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Warning("Assembly resolution raised an error. Requested=" + args.Name + "; " + ExceptionDiagnostics.Format(ex));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an identity-compatible DLL without loading it into the current AppDomain.
|
||||
/// </summary>
|
||||
/// <param name="simpleName">Requested simple assembly name.</param>
|
||||
/// <param name="requested">Full requested identity, or null to accept any installed version.</param>
|
||||
/// <param name="directories">Trusted directories to search.</param>
|
||||
/// <returns>The matching DLL path, or null.</returns>
|
||||
internal static string FindCandidateFile(string simpleName, AssemblyName requested, IEnumerable<string> directories)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(simpleName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
foreach (var directory in NormalizeDirectories(directories))
|
||||
{
|
||||
var candidate = Path.Combine(directory, simpleName + ".dll");
|
||||
if (!File.Exists(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
var actual = AssemblyName.GetAssemblyName(candidate);
|
||||
if (requested == null
|
||||
? string.Equals(actual.Name, simpleName, StringComparison.OrdinalIgnoreCase)
|
||||
: AssemblyIdentityMatches(requested, actual))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A native, corrupt or unrelated file is never loaded merely because its file name matches.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Compares simple name, requested version, culture and public key token.</summary>
|
||||
private static bool AssemblyIdentityMatches(AssemblyName requested, AssemblyName actual)
|
||||
{
|
||||
if (requested == null || actual == null
|
||||
|| !string.Equals(requested.Name, actual.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (requested.Version != null && actual.Version != requested.Version)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(requested.CultureName)
|
||||
&& !string.Equals(requested.CultureName, actual.CultureName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var expectedToken = requested.GetPublicKeyToken();
|
||||
var actualToken = actual.GetPublicKeyToken();
|
||||
return expectedToken == null || expectedToken.Length == 0 || TokensEqual(expectedToken, actualToken);
|
||||
}
|
||||
|
||||
/// <summary>Compares strong-name public key tokens.</summary>
|
||||
private static bool TokensEqual(byte[] left, byte[] right)
|
||||
{
|
||||
if (left == null || right == null || left.Length != right.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < left.Length; i++)
|
||||
{
|
||||
if (left[i] != right[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Discovers BizTalk, ScheduledTask and explicitly configured directories.</summary>
|
||||
private static IEnumerable<string> DiscoverSearchDirectories()
|
||||
{
|
||||
var result = new List<string>();
|
||||
AddConfiguredDirectories(result);
|
||||
AddRegistryDirectories(result);
|
||||
|
||||
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||||
AddMatchingDirectories(result, programFilesX86, "Microsoft BizTalk Server*");
|
||||
AddMatchingDirectories(result, programFilesX86, "BizTalk ScheduledTask Adapter*");
|
||||
AddMatchingDirectories(result, programFilesX86, "Biztalk ScheduledTask Adapter*");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Adds semicolon-delimited paths from App.config after environment expansion.</summary>
|
||||
private static void AddConfiguredDirectories(ICollection<string> result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configured = ConfigurationManager.AppSettings["AdapterAssemblySearchPaths"];
|
||||
if (string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var value in configured.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
result.Add(Environment.ExpandEnvironmentVariables(value.Trim()));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Registry and conventional installation directories remain available.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds known assembly-path values from both registry views.</summary>
|
||||
private static void AddRegistryDirectories(ICollection<string> result)
|
||||
{
|
||||
var views = new[] { RegistryView.Registry32, RegistryView.Registry64 };
|
||||
foreach (var view in views)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
|
||||
{
|
||||
AddRegistryTreeDirectories(baseKey, @"SOFTWARE\Microsoft\BizTalk Server\3.0", result, 0);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Registry discovery is best-effort and never blocks normal BizTalk adapters.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads installation and assembly directories from a shallow BizTalk registry tree.</summary>
|
||||
private static void AddRegistryTreeDirectories(RegistryKey baseKey, string subKeyName, ICollection<string> result, int depth)
|
||||
{
|
||||
if (depth > 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
using (var key = baseKey.OpenSubKey(subKeyName, false))
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var valueName in key.GetValueNames())
|
||||
{
|
||||
var value = key.GetValue(valueName) as string;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
value = Environment.ExpandEnvironmentVariables(value.Trim().Trim('"'));
|
||||
if (value.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.Add(Path.GetDirectoryName(value));
|
||||
}
|
||||
else if (Directory.Exists(value))
|
||||
{
|
||||
result.Add(value);
|
||||
}
|
||||
}
|
||||
foreach (var child in key.GetSubKeyNames())
|
||||
{
|
||||
AddRegistryTreeDirectories(baseKey, subKeyName + "\\" + child, result, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds directories matching one non-recursive product-folder pattern.</summary>
|
||||
private static void AddMatchingDirectories(ICollection<string> result, string parent, string pattern)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(parent))
|
||||
{
|
||||
foreach (var directory in Directory.GetDirectories(parent, pattern, SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
result.Add(directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A denied optional discovery path is reported later if Scheduler preflight is required.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Normalizes, de-duplicates and filters existing directories.</summary>
|
||||
private static IEnumerable<string> NormalizeDirectories(IEnumerable<string> directories)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var value in directories ?? Enumerable.Empty<string>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string fullPath;
|
||||
try
|
||||
{
|
||||
fullPath = Path.GetFullPath(value.Trim().Trim('"')).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (Directory.Exists(fullPath) && seen.Add(fullPath))
|
||||
{
|
||||
yield return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Builds a diagnostic assembly description.</summary>
|
||||
private static string DescribeAssembly(Assembly assembly)
|
||||
{
|
||||
return assembly.FullName + "; Location=" + assembly.Location;
|
||||
}
|
||||
|
||||
/// <summary>Writes informational diagnostics when a logger is available.</summary>
|
||||
private void Info(string message) { if (_logger != null) _logger.Info(message); }
|
||||
|
||||
/// <summary>Writes warning diagnostics when a logger is available.</summary>
|
||||
private void Warning(string message) { if (_logger != null) _logger.Warning(message); }
|
||||
|
||||
/// <summary>Writes successful dependency diagnostics when a logger is available.</summary>
|
||||
private void Success(string message) { if (_logger != null) _logger.Success(message); }
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
/// <summary>
|
||||
/// Current tool version written into generated snapshots.
|
||||
/// </summary>
|
||||
public const string Version = "2.2.4-net461";
|
||||
public const string Version = "2.3.0-net461";
|
||||
|
||||
/// <summary>
|
||||
/// Fallback application name used when WMI does not expose an application property.
|
||||
@@ -192,7 +192,9 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
|
||||
{
|
||||
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null));
|
||||
var step = Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null);
|
||||
SetReceiveTransport(step, item);
|
||||
plan.Steps.Add(step);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +308,9 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.ReceiveLocations)
|
||||
{
|
||||
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null));
|
||||
var step = Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null);
|
||||
SetReceiveTransport(step, item);
|
||||
plan.Steps.Add(step);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,6 +469,9 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
/// <summary>Operation logger.</summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>Process-local resolver for adapter validation dependencies.</summary>
|
||||
private readonly AdapterAssemblyResolver _assemblyResolver;
|
||||
|
||||
/// <summary>Lazily connected BizTalk WMI client.</summary>
|
||||
private BizTalkWmiClient _client;
|
||||
|
||||
@@ -475,6 +482,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
||||
_logger = logger;
|
||||
_assemblyResolver = new AdapterAssemblyResolver(logger);
|
||||
}
|
||||
|
||||
/// <summary>Executes one Windows-service or BizTalk-WMI step state-aware.</summary>
|
||||
@@ -499,6 +507,10 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
return RuntimeStepOutcome.AlreadySatisfied;
|
||||
}
|
||||
if (IsScheduledTaskReceiveLocation(step))
|
||||
{
|
||||
_assemblyResolver.PrepareScheduledTaskAdapter();
|
||||
}
|
||||
ExecuteWmiStep(instance, step, options);
|
||||
return RuntimeStepOutcome.Succeeded;
|
||||
}
|
||||
@@ -514,6 +526,20 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
_client.Dispose();
|
||||
_client = null;
|
||||
}
|
||||
_assemblyResolver.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Detects the third-party ScheduledTask adapter from captured plan metadata.</summary>
|
||||
/// <param name="step">The receive-location step.</param>
|
||||
/// <returns>True when Scheduler dependency preflight is required.</returns>
|
||||
private static bool IsScheduledTaskReceiveLocation(OperationStep step)
|
||||
{
|
||||
return step != null
|
||||
&& string.Equals(step.Kind, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
|
||||
&& ((!string.IsNullOrWhiteSpace(step.Address)
|
||||
&& step.Address.TrimStart().StartsWith("scheduler:", StringComparison.OrdinalIgnoreCase))
|
||||
|| (!string.IsNullOrWhiteSpace(step.AdapterName)
|
||||
&& step.AdapterName.IndexOf("schedul", StringComparison.OrdinalIgnoreCase) >= 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -735,6 +761,15 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Copies receive transport metadata into a durable operation step.</summary>
|
||||
/// <param name="step">The plan step to enrich.</param>
|
||||
/// <param name="receiveLocation">The captured receive location.</param>
|
||||
private static void SetReceiveTransport(OperationStep step, ReceiveLocationState receiveLocation)
|
||||
{
|
||||
step.AdapterName = receiveLocation == null ? null : receiveLocation.AdapterName;
|
||||
step.Address = receiveLocation == null ? null : receiveLocation.Address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts integer method arguments into the object array required by WMI.
|
||||
/// </summary>
|
||||
@@ -773,6 +808,8 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
+ ", class=" + step.WmiClass
|
||||
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
||||
+ ", method=" + step.MethodName
|
||||
+ (string.IsNullOrWhiteSpace(step.AdapterName) ? string.Empty : ", adapter=" + step.AdapterName)
|
||||
+ (string.IsNullOrWhiteSpace(step.Address) ? string.Empty : ", address=" + step.Address)
|
||||
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates support-ready, single-line exception diagnostics for the GUI, durable
|
||||
/// execution reports and rolling log files.
|
||||
/// </summary>
|
||||
internal static class ExceptionDiagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Formats the complete exception chain including type, HRESULT, fusion details
|
||||
/// and stack trace without embedding physical line breaks in the daily log.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception to format.</param>
|
||||
/// <returns>A detailed single-line diagnostic string.</returns>
|
||||
public static string Format(Exception exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
return "Unknown operation error.";
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
var current = exception;
|
||||
var depth = 0;
|
||||
while (current != null)
|
||||
{
|
||||
var label = depth == 0 ? "Exception" : "InnerException[" + depth.ToString(CultureInfo.InvariantCulture) + "]";
|
||||
var part = label
|
||||
+ " Type=" + current.GetType().FullName
|
||||
+ "; HResult=0x" + current.HResult.ToString("X8", CultureInfo.InvariantCulture)
|
||||
+ "; Message=" + Flatten(current.Message);
|
||||
|
||||
var fileNotFound = current as FileNotFoundException;
|
||||
if (fileNotFound != null && !string.IsNullOrWhiteSpace(fileNotFound.FusionLog))
|
||||
{
|
||||
part += "; FusionLog=" + Flatten(fileNotFound.FusionLog);
|
||||
}
|
||||
var fileLoad = current as FileLoadException;
|
||||
if (fileLoad != null && !string.IsNullOrWhiteSpace(fileLoad.FusionLog))
|
||||
{
|
||||
part += "; FusionLog=" + Flatten(fileLoad.FusionLog);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(current.StackTrace))
|
||||
{
|
||||
part += "; StackTrace=" + Flatten(current.StackTrace);
|
||||
}
|
||||
|
||||
parts.Add(part);
|
||||
current = current.InnerException;
|
||||
depth++;
|
||||
}
|
||||
return string.Join(" | ", parts.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Replaces physical control characters with readable escape sequences.</summary>
|
||||
/// <param name="value">The text to flatten.</param>
|
||||
/// <returns>Single-line text suitable for one durable log record.</returns>
|
||||
private static string Flatten(string value)
|
||||
{
|
||||
return (value ?? string.Empty)
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\t", "\\t");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
@@ -68,20 +71,18 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
private const string LogFileExtension = ".log";
|
||||
|
||||
/// <summary>
|
||||
/// Number of daily log files retained, including the current day.
|
||||
/// Number of calendar days retained, including the current day.
|
||||
/// </summary>
|
||||
private const int RetentionDays = 5;
|
||||
internal const int RetentionDays = 30;
|
||||
|
||||
/// <summary>Maximum number of historical records restored into the GUI by default.</summary>
|
||||
public const int DefaultGridHistoryLimit = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
@@ -97,12 +98,24 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
/// </summary>
|
||||
/// <param name="sink">Optional callback that receives entries for display.</param>
|
||||
public OperationLogger(Action<LogEntry> sink)
|
||||
: this(sink, ResolveLogDirectory())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a logger with an explicit directory for regression tests.</summary>
|
||||
/// <param name="sink">Optional callback that receives new entries.</param>
|
||||
/// <param name="logDirectory">Directory used for plain and compressed logs.</param>
|
||||
internal OperationLogger(Action<LogEntry> sink, string logDirectory)
|
||||
{
|
||||
_sink = sink;
|
||||
_logDirectory = ResolveLogDirectory();
|
||||
CleanupOldLogs();
|
||||
_logDirectory = Path.GetFullPath(logDirectory);
|
||||
Directory.CreateDirectory(_logDirectory);
|
||||
MaintainLogs(DateTime.Now.Date);
|
||||
}
|
||||
|
||||
/// <summary>Gets the directory containing active and compressed runtime logs.</summary>
|
||||
public string LogDirectory { get { return _logDirectory; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the daily log file for the current date.
|
||||
/// </summary>
|
||||
@@ -114,6 +127,42 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads retained plain and GZip-compressed records for restoring the operation grid.
|
||||
/// Malformed legacy lines are ignored without affecting current logging.
|
||||
/// </summary>
|
||||
/// <param name="maximumEntries">Maximum newest entries to return.</param>
|
||||
/// <returns>Chronologically ordered retained log entries.</returns>
|
||||
public IList<LogEntry> ReadRecentEntries(int maximumEntries)
|
||||
{
|
||||
if (maximumEntries <= 0)
|
||||
{
|
||||
return new List<LogEntry>();
|
||||
}
|
||||
|
||||
var entries = new List<LogEntry>();
|
||||
try
|
||||
{
|
||||
var files = Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension + "*")
|
||||
.Where(IsSupportedLogFile)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
foreach (var file in files)
|
||||
{
|
||||
ReadEntries(file, entries);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Historical display is optional; current operations and file logging continue.
|
||||
}
|
||||
|
||||
return entries
|
||||
.OrderBy(x => x.Timestamp)
|
||||
.Skip(Math.Max(0, entries.Count - maximumEntries))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an informational entry.
|
||||
/// </summary>
|
||||
@@ -202,7 +251,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
"[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
|
||||
entry.Timestamp,
|
||||
entry.Level.ToString().ToUpperInvariant(),
|
||||
entry.Message,
|
||||
(entry.Message ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n"),
|
||||
Environment.NewLine);
|
||||
|
||||
lock (FileLock)
|
||||
@@ -217,24 +266,29 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes log files older than the configured retention window.
|
||||
/// Compresses completed daily logs and removes all records outside retention.
|
||||
/// </summary>
|
||||
private void CleanupOldLogs()
|
||||
/// <param name="today">The current local date.</param>
|
||||
internal void MaintainLogs(DateTime today)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _cleanupDone, 1) == 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cutoff = DateTime.Now.Date.AddDays(-(RetentionDays - 1));
|
||||
foreach (var file in Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension))
|
||||
var cutoff = today.Date.AddDays(-(RetentionDays - 1));
|
||||
foreach (var file in Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension + "*"))
|
||||
{
|
||||
var lastWrite = File.GetLastWriteTime(file);
|
||||
if (lastWrite.Date < cutoff)
|
||||
DateTime fileDate;
|
||||
if (!TryGetLogDate(file, out fileDate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (fileDate < cutoff)
|
||||
{
|
||||
File.Delete(file);
|
||||
continue;
|
||||
}
|
||||
if (fileDate < today.Date && file.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Compress(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,6 +298,117 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Compresses a completed log atomically and removes the plain source afterward.</summary>
|
||||
/// <param name="source">The completed plain log.</param>
|
||||
private static void Compress(string source)
|
||||
{
|
||||
var target = source + ".gz";
|
||||
if (File.Exists(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var temporary = target + ".tmp." + Guid.NewGuid().ToString("N");
|
||||
try
|
||||
{
|
||||
using (var input = File.OpenRead(source))
|
||||
using (var output = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
using (var gzip = new GZipStream(output, CompressionLevel.Optimal))
|
||||
{
|
||||
input.CopyTo(gzip);
|
||||
}
|
||||
File.Move(temporary, target);
|
||||
File.Delete(source);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporary))
|
||||
{
|
||||
File.Delete(temporary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads one supported plain or compressed file into an entry collection.</summary>
|
||||
private static void ReadEntries(string path, ICollection<LogEntry> entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
using (var payload = path.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)
|
||||
? (Stream)new GZipStream(file, CompressionMode.Decompress)
|
||||
: file)
|
||||
using (var reader = new StreamReader(payload, Encoding.UTF8, true))
|
||||
{
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
LogEntry entry;
|
||||
if (TryParse(line, out entry))
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// One unreadable archive must not hide all other retained history.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parses the stable daily log line format.</summary>
|
||||
/// <param name="line">One physical line.</param>
|
||||
/// <param name="entry">Parsed entry when successful.</param>
|
||||
/// <returns>True for a valid record.</returns>
|
||||
internal static bool TryParse(string line, out LogEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(line) || line.Length < 29 || line[0] != '[' || line[21] != '[')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var timestampEnd = line.IndexOf(']');
|
||||
var levelEnd = line.IndexOf(']', timestampEnd + 1);
|
||||
DateTime timestamp;
|
||||
LogLevel level;
|
||||
if (timestampEnd != 20 || levelEnd < 0
|
||||
|| !DateTime.TryParseExact(line.Substring(1, 19), "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out timestamp)
|
||||
|| !Enum.TryParse(line.Substring(timestampEnd + 2, levelEnd - timestampEnd - 2), true, out level))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var messageStart = levelEnd + 1;
|
||||
if (messageStart < line.Length && line[messageStart] == ' ')
|
||||
{
|
||||
messageStart++;
|
||||
}
|
||||
entry = new LogEntry { Timestamp = timestamp, Level = level, Message = line.Substring(messageStart) };
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Checks the supported exact file suffixes.</summary>
|
||||
private static bool IsSupportedLogFile(string path)
|
||||
{
|
||||
return path.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith(LogFileExtension + ".gz", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Extracts the calendar date from a stable log file name.</summary>
|
||||
private static bool TryGetLogDate(string path, out DateTime date)
|
||||
{
|
||||
var name = Path.GetFileName(path);
|
||||
var suffixLength = name.EndsWith(LogFileExtension + ".gz", StringComparison.OrdinalIgnoreCase)
|
||||
? (LogFileExtension + ".gz").Length
|
||||
: name.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase) ? LogFileExtension.Length : 0;
|
||||
if (suffixLength == 0 || !name.StartsWith(LogFilePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
date = default(DateTime);
|
||||
return false;
|
||||
}
|
||||
var value = name.Substring(LogFilePrefix.Length, name.Length - LogFilePrefix.Length - suffixLength);
|
||||
return DateTime.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ermittelt das bevorzugte maschinenweite Logverzeichnis mit Rückfall auf das EXE-Verzeichnis.
|
||||
/// </summary>
|
||||
|
||||
@@ -179,6 +179,8 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
+ ", class=" + step.WmiClass
|
||||
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
||||
+ ", method=" + step.MethodName
|
||||
+ (string.IsNullOrWhiteSpace(step.AdapterName) ? string.Empty : ", adapter=" + step.AdapterName)
|
||||
+ (string.IsNullOrWhiteSpace(step.Address) ? string.Empty : ", address=" + step.Address)
|
||||
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
|
||||
+ "]";
|
||||
}
|
||||
@@ -188,21 +190,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
/// <returns>A single diagnostic message.</returns>
|
||||
internal static string FormatException(Exception exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
return "Unknown operation error.";
|
||||
}
|
||||
var message = exception.Message;
|
||||
var inner = exception.InnerException;
|
||||
while (inner != null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
message += " Inner error: " + inner.Message;
|
||||
}
|
||||
inner = inner.InnerException;
|
||||
}
|
||||
return message;
|
||||
return ExceptionDiagnostics.Format(exception);
|
||||
}
|
||||
|
||||
/// <summary>Writes an informational message when a logger is available.</summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -125,6 +126,9 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
/// </summary>
|
||||
private Button _clearButton;
|
||||
|
||||
/// <summary>Button that opens the persistent runtime log directory.</summary>
|
||||
private Button _openLogsButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that closes the application.
|
||||
/// </summary>
|
||||
@@ -150,6 +154,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
_service = new BizTalkOperationService(_logger);
|
||||
BuildUi();
|
||||
FormClosing += MainFormClosing;
|
||||
LoadLogHistory();
|
||||
_logger.Info("Log file: " + _logger.LogFilePath);
|
||||
}
|
||||
|
||||
@@ -272,6 +277,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
_emergencyRestoreButton = ActionButton("Emergency Restore", EmergencyRestoreClick);
|
||||
_emergencyRestoreButton.Width = 142;
|
||||
_clearButton = ActionButton("Clear", ClearClick);
|
||||
_openLogsButton = ActionButton("Log Folder", OpenLogsClick);
|
||||
_closeButton = ActionButton("Close", CloseClick);
|
||||
|
||||
panel.Controls.Add(_diagnoseButton);
|
||||
@@ -283,6 +289,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
panel.Controls.Add(_validateStateButton);
|
||||
panel.Controls.Add(_emergencyRestoreButton);
|
||||
panel.Controls.Add(_clearButton);
|
||||
panel.Controls.Add(_openLogsButton);
|
||||
panel.Controls.Add(_closeButton);
|
||||
return panel;
|
||||
}
|
||||
@@ -306,7 +313,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
statusPage.Controls.Add(_statusGrid);
|
||||
|
||||
ConfigureGrid(_logGrid);
|
||||
_logGrid.Columns.Add("Time", "Time");
|
||||
_logGrid.Columns.Add("Timestamp", "Timestamp");
|
||||
_logGrid.Columns.Add("Level", "Level");
|
||||
_logGrid.Columns.Add("Message", "Message");
|
||||
_logGrid.Columns[2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
||||
@@ -577,6 +584,22 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
_statusLabel.Text = "Ready.";
|
||||
}
|
||||
|
||||
/// <summary>Opens the persistent local runtime log directory in Windows Explorer.</summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void OpenLogsClick(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(_logger.LogDirectory);
|
||||
Process.Start("explorer.exe", _logger.LogDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Could not open runtime log directory. " + ExceptionDiagnostics.Format(ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Close button click.
|
||||
/// </summary>
|
||||
@@ -816,24 +839,53 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
{
|
||||
var index = _logGrid.Rows.Add(entry.Timestamp.ToString("HH:mm:ss"), entry.Level.ToString(), entry.Message);
|
||||
var row = _logGrid.Rows[index];
|
||||
if (entry.Level == LogLevel.Error)
|
||||
{
|
||||
row.DefaultCellStyle.ForeColor = Color.DarkRed;
|
||||
}
|
||||
else if (entry.Level == LogLevel.Warning)
|
||||
{
|
||||
row.DefaultCellStyle.ForeColor = Color.DarkOrange;
|
||||
}
|
||||
else if (entry.Level == LogLevel.Success)
|
||||
{
|
||||
row.DefaultCellStyle.ForeColor = Color.DarkGreen;
|
||||
}
|
||||
AddLogRow(entry);
|
||||
_logGrid.FirstDisplayedScrollingRowIndex = Math.Max(0, _logGrid.Rows.Count - 1);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Restores retained plain and compressed log records into the grid at startup.</summary>
|
||||
private void LoadLogHistory()
|
||||
{
|
||||
var entries = _logger.ReadRecentEntries(OperationLogger.DefaultGridHistoryLimit);
|
||||
_logGrid.SuspendLayout();
|
||||
try
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
AddLogRow(entry);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logGrid.ResumeLayout();
|
||||
}
|
||||
if (_logGrid.Rows.Count > 0)
|
||||
{
|
||||
_logGrid.FirstDisplayedScrollingRowIndex = _logGrid.Rows.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds and colors one retained or live log row.</summary>
|
||||
/// <param name="entry">The log entry to display.</param>
|
||||
private void AddLogRow(LogEntry entry)
|
||||
{
|
||||
var index = _logGrid.Rows.Add(entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"), entry.Level.ToString(), entry.Message);
|
||||
var row = _logGrid.Rows[index];
|
||||
if (entry.Level == LogLevel.Error)
|
||||
{
|
||||
row.DefaultCellStyle.ForeColor = Color.DarkRed;
|
||||
}
|
||||
else if (entry.Level == LogLevel.Warning)
|
||||
{
|
||||
row.DefaultCellStyle.ForeColor = Color.DarkOrange;
|
||||
}
|
||||
else if (entry.Level == LogLevel.Success)
|
||||
{
|
||||
row.DefaultCellStyle.ForeColor = Color.DarkGreen;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables action buttons and updates the status strip.
|
||||
/// </summary>
|
||||
@@ -853,6 +905,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
_validateStateButton.Enabled = !busy;
|
||||
_emergencyRestoreButton.Enabled = !busy;
|
||||
_clearButton.Enabled = !busy;
|
||||
_openLogsButton.Enabled = !busy;
|
||||
_closeButton.Enabled = !busy;
|
||||
_statusLabel.Text = status;
|
||||
});
|
||||
@@ -1071,19 +1124,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
return "Operation failed.";
|
||||
}
|
||||
|
||||
var message = ex.Message;
|
||||
var inner = ex.InnerException;
|
||||
while (inner != null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
message += " Inner error: " + inner.Message;
|
||||
}
|
||||
|
||||
inner = inner.InnerException;
|
||||
}
|
||||
|
||||
return message;
|
||||
return ExceptionDiagnostics.Format(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="2.2.4.0" name="BizTalkPlatformManagementTool" />
|
||||
<assemblyIdentity version="2.3.0.0" name="BizTalkPlatformManagementTool" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
|
||||
Reference in New Issue
Block a user