diff --git a/build/_build.csproj b/build/_build.csproj
index 9573e682..a1f4a547 100644
--- a/build/_build.csproj
+++ b/build/_build.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index d2dc0368..918e0294 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -22,8 +22,8 @@
-
-
+
+
diff --git a/src/LogExpert.Tests/Services/ProjectFileHandlerTests.cs b/src/LogExpert.Tests/Services/ProjectFileHandlerTests.cs
new file mode 100644
index 00000000..205d08f3
--- /dev/null
+++ b/src/LogExpert.Tests/Services/ProjectFileHandlerTests.cs
@@ -0,0 +1,652 @@
+using System.Runtime.Versioning;
+
+using LogExpert.Core.Classes.Persister;
+using LogExpert.Core.Config;
+using LogExpert.Core.Interfaces;
+using LogExpert.UI.Controls.LogWindow;
+using LogExpert.UI.Interface;
+using LogExpert.UI.Services.FileOperationService;
+using LogExpert.UI.Services.ProjectFileHandlerService;
+
+using Moq;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests.Services;
+
+[TestFixture]
+[Apartment(ApartmentState.STA)]
+[SupportedOSPlatform("windows")]
+internal class ProjectFileHandlerTests : IDisposable
+{
+ private Mock _pluginRegistryMock = null!;
+ private List _addFileTabCalls = null!;
+ private Settings _settings;
+ private Mock _configManagerMock;
+ private LogWindow _stubLogWindow = null!;
+ private ProjectFileHandler _sut = null!;
+
+ private bool _disposed;
+
+ [SetUp]
+ public void SetUp ()
+ {
+ // Ensure the static PluginRegistry singleton is initialized.
+ // LogWindow's constructor accesses PluginRegistry.Instance.RegisteredColumnizers[0] directly.
+ _ = PluginRegistry.PluginRegistry.Create(Path.GetTempPath(), 250);
+
+ _pluginRegistryMock = new Mock();
+ _configManagerMock = new Mock();
+ _addFileTabCalls = [];
+
+ _settings = new Settings();
+ _ = _configManagerMock.Setup(cm => cm.Settings).Returns(_settings);
+
+ _ = _pluginRegistryMock.Setup(pr => pr.RegisteredColumnizers).Returns([]);
+
+ // Create a minimal stub LogWindow for the callback.
+ // LogWindow requires an ILogWindowCoordinator
+ var coordinatorMock = new Mock();
+ _stubLogWindow = new LogWindow(
+ coordinatorMock.Object,
+ "stub.log",
+ isTempFile: false,
+ forcePersistenceLoading: false,
+ configManager: _configManagerMock.Object);
+
+ _sut = new ProjectFileHandler(
+ _pluginRegistryMock.Object,
+ request =>
+ {
+ _addFileTabCalls.Add(request);
+ return _stubLogWindow;
+ });
+ }
+
+ [TearDown]
+ public void TearDown ()
+ {
+ _stubLogWindow?.Dispose();
+ }
+
+ #region LoadProject — Phase 1 tests
+
+ [Test]
+ public void LoadProject_ValidProjectFile_ReturnsSuccess ()
+ {
+ // Arrange: create a real log file so validation passes
+ var logFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".log");
+ File.WriteAllText(logFile, "test log line");
+ var tempFile = CreateTempProjectFile([logFile], layoutXml: null);
+
+ try
+ {
+ // Act
+ var outcome = _sut.LoadProject(tempFile);
+
+ // Assert
+ Assert.That(outcome.Status, Is.EqualTo(ProjectLoadOutcome.LoadStatus.Success));
+ Assert.That(outcome.ProjectData, Is.Not.Null);
+ Assert.That(outcome.ProjectData!.FileNames, Has.Count.EqualTo(1));
+ Assert.That(outcome.ErrorMessage, Is.Null);
+ Assert.That(outcome.ValidationResult, Is.Null);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ File.Delete(logFile);
+ }
+ }
+
+ [Test]
+ public void LoadProject_ValidProjectWithLayoutXml_HasLayoutDataIsTrue ()
+ {
+ // Arrange: create a real log file so validation passes
+ var logFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".log");
+ File.WriteAllText(logFile, "test log line");
+ var tempFile = CreateTempProjectFile([logFile], layoutXml: "");
+
+ try
+ {
+ // Act
+ var outcome = _sut.LoadProject(tempFile);
+
+ // Assert
+ Assert.That(outcome.Status, Is.EqualTo(ProjectLoadOutcome.LoadStatus.Success));
+ Assert.That(outcome.HasLayoutData, Is.True);
+ Assert.That(outcome.LayoutXml, Is.EqualTo(""));
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ File.Delete(logFile);
+ }
+ }
+
+ [Test]
+ public void LoadProject_ValidProjectWithoutLayoutXml_HasLayoutDataIsFalse ()
+ {
+ // Arrange
+ var tempFile = CreateTempProjectFile(["C:\\logs\\app.log"], layoutXml: null);
+
+ try
+ {
+ // Act
+ var outcome = _sut.LoadProject(tempFile);
+
+ // Assert
+ Assert.That(outcome.HasLayoutData, Is.False);
+ Assert.That(outcome.LayoutXml, Is.Null);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ [Test]
+ public void LoadProject_ProjectWithMissingFiles_ReturnsNeedsIntervention ()
+ {
+ // Arrange: use file paths that do not exist on disk
+ var tempFile = CreateTempProjectFile(
+ ["C:\\nonexistent\\path\\missing.log", "C:\\also\\missing.log"],
+ layoutXml: null);
+
+ try
+ {
+ // Act
+ var outcome = _sut.LoadProject(tempFile);
+
+ // Assert
+ Assert.That(outcome.Status, Is.EqualTo(ProjectLoadOutcome.LoadStatus.NeedsIntervention));
+ Assert.That(outcome.ProjectData, Is.Not.Null);
+ Assert.That(outcome.ValidationResult, Is.Not.Null);
+ Assert.That(outcome.ValidationResult!.HasMissingFiles, Is.True);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ [Test]
+ public void LoadProject_CorruptFile_ReturnsError ()
+ {
+ // Arrange: write garbage content
+ var tempFile = Path.GetTempFileName();
+ File.WriteAllText(tempFile, "{{{{not json not xml}}}}");
+
+ try
+ {
+ // Act
+ var outcome = _sut.LoadProject(tempFile);
+
+ // Assert
+ Assert.That(outcome.Status, Is.EqualTo(ProjectLoadOutcome.LoadStatus.Error));
+ Assert.That(outcome.ErrorMessage, Is.Not.Null.And.Not.Empty);
+ Assert.That(outcome.ProjectData, Is.Null);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ [Test]
+ public void LoadProject_NonexistentFile_ReturnsError ()
+ {
+ // Arrange
+ var fakePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".lxj");
+
+ // Act
+ var outcome = _sut.LoadProject(fakePath);
+
+ // Assert
+ Assert.That(outcome.Status, Is.EqualTo(ProjectLoadOutcome.LoadStatus.Error));
+ Assert.That(outcome.ErrorMessage, Is.Not.Null.And.Not.Empty);
+ }
+
+ [Test]
+ public void LoadProject_EmptyFileList_ReturnsEmptyProject ()
+ {
+ // Arrange: valid JSON but with zero files
+ var tempFile = CreateTempProjectFile([], layoutXml: null);
+
+ try
+ {
+ // Act
+ var outcome = _sut.LoadProject(tempFile);
+
+ // Assert
+ Assert.That(outcome.Status, Is.EqualTo(ProjectLoadOutcome.LoadStatus.EmptyProject));
+ Assert.That(outcome.ErrorMessage, Is.Not.Null.And.Not.Empty);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ [Test]
+ public void LoadProject_DoesNotInvokeAddFileTabCallback ()
+ {
+ // Arrange
+ var tempFile = CreateTempProjectFile(["C:\\logs\\app.log"], layoutXml: null);
+
+ try
+ {
+ // Act
+ _ = _sut.LoadProject(tempFile);
+
+ // Assert — phase 1 must never open tabs
+ Assert.That(_addFileTabCalls, Is.Empty);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ #endregion
+
+ #region ContinueLoad tests
+
+ [Test]
+ public void ContinueLoad_SuccessNoLayout_CallsAddFileTabWithDoNotAddToDockPanelFalse ()
+ {
+ // Arrange
+ var outcome = CreateSuccessOutcome(["C:\\logs\\app.log"], layoutXml: null);
+
+ // Act
+ var result = _sut.ContinueLoad(outcome, resolution: null, restoreLayout: true);
+
+ // Assert
+ Assert.That(result.OpenedTabs, Is.True);
+ Assert.That(_addFileTabCalls, Has.Count.EqualTo(1));
+ Assert.That(_addFileTabCalls[0].DoNotAddToDockPanel, Is.False);
+ Assert.That(_addFileTabCalls[0].FileName, Is.EqualTo("C:\\logs\\app.log"));
+ Assert.That(_addFileTabCalls[0].ForcePersistenceLoading, Is.True);
+ }
+
+ [Test]
+ public void ContinueLoad_SuccessWithLayout_CallsAddFileTabWithDoNotAddToDockPanelTrue ()
+ {
+ // Arrange
+ var outcome = CreateSuccessOutcome(["C:\\logs\\app.log"], layoutXml: "");
+
+ // Act
+ var result = _sut.ContinueLoad(outcome, resolution: null, restoreLayout: true);
+
+ // Assert
+ Assert.That(result.OpenedTabs, Is.True);
+ Assert.That(_addFileTabCalls, Has.Count.EqualTo(1));
+ Assert.That(_addFileTabCalls[0].DoNotAddToDockPanel, Is.True);
+ }
+
+ [Test]
+ public void ContinueLoad_SuccessWithLayoutButRestoreLayoutFalse_DoNotAddToDockPanelIsFalse ()
+ {
+ // Arrange
+ var outcome = CreateSuccessOutcome(["C:\\logs\\app.log"], layoutXml: "");
+
+ // Act
+ _ = _sut.ContinueLoad(outcome, resolution: null, restoreLayout: false);
+
+ // Assert
+ Assert.That(_addFileTabCalls[0].DoNotAddToDockPanel, Is.False);
+ }
+
+ [Test]
+ public void ContinueLoad_MultipleFiles_CallsAddFileTabForEachFile ()
+ {
+ // Arrange
+ var outcome = CreateSuccessOutcome(["C:\\logs\\a.log", "C:\\logs\\b.log", "C:\\logs\\c.log"], layoutXml: null);
+
+ // Act
+ var result = _sut.ContinueLoad(outcome, resolution: null, restoreLayout: true);
+
+ // Assert
+ Assert.That(result.OpenedTabs, Is.True);
+ Assert.That(_addFileTabCalls, Has.Count.EqualTo(3));
+ Assert.That(_addFileTabCalls[0].FileName, Is.EqualTo("C:\\logs\\a.log"));
+ Assert.That(_addFileTabCalls[1].FileName, Is.EqualTo("C:\\logs\\b.log"));
+ Assert.That(_addFileTabCalls[2].FileName, Is.EqualTo("C:\\logs\\c.log"));
+ }
+
+ [Test]
+ public void ContinueLoad_WithAlternatives_ReplacesFilePathsBeforeOpening ()
+ {
+ // Arrange
+ var outcome = CreateOutcome(
+ ProjectLoadOutcome.LoadStatus.NeedsIntervention,
+ ["C:\\old\\missing.log", "C:\\logs\\existing.log"],
+ layoutXml: null);
+
+ var resolution = new MissingFilesResolution
+ {
+ SelectedAlternatives = new Dictionary
+ {
+ ["C:\\old\\missing.log"] = "D:\\new\\found.log"
+ }
+ };
+
+ // Act
+ _ = _sut.ContinueLoad(outcome, resolution, restoreLayout: true);
+
+ // Assert
+ Assert.That(_addFileTabCalls, Has.Count.EqualTo(2));
+ Assert.That(_addFileTabCalls[0].FileName, Is.EqualTo("D:\\new\\found.log"));
+ Assert.That(_addFileTabCalls[1].FileName, Is.EqualTo("C:\\logs\\existing.log"));
+ }
+
+ [Test]
+ public void ContinueLoad_DuplicateFileWithAlternative_ReplacesBothOccurrences ()
+ {
+ // Arrange
+ var outcome = CreateOutcome(
+ ProjectLoadOutcome.LoadStatus.NeedsIntervention,
+ ["C:\\missing.log", "C:\\missing.log"],
+ layoutXml: null);
+
+ var resolution = new MissingFilesResolution
+ {
+ SelectedAlternatives = new Dictionary
+ {
+ ["C:\\missing.log"] = "D:\\found.log"
+ }
+ };
+
+ // Act
+ _ = _sut.ContinueLoad(outcome, resolution, restoreLayout: true);
+
+ // Assert
+ Assert.That(_addFileTabCalls, Has.Count.EqualTo(2));
+ Assert.That(_addFileTabCalls[0].FileName, Is.EqualTo("D:\\found.log"));
+ Assert.That(_addFileTabCalls[1].FileName, Is.EqualTo("D:\\found.log"));
+ }
+
+ [Test]
+ public void ContinueLoad_CloseAllTabs_ReturnsTrueInResult ()
+ {
+ // Arrange
+ var outcome = CreateOutcome(
+ ProjectLoadOutcome.LoadStatus.NeedsIntervention,
+ ["C:\\logs\\app.log"],
+ layoutXml: "");
+
+ var resolution = new MissingFilesResolution { CloseAllTabs = true };
+
+ // Act
+ var result = _sut.ContinueLoad(outcome, resolution, restoreLayout: true);
+
+ // Assert
+ Assert.That(result.CloseAllTabs, Is.True);
+ Assert.That(result.OpenedTabs, Is.True);
+ Assert.That(result.OpenInNewWindowFiles, Is.Null);
+ }
+
+ [Test]
+ public void ContinueLoad_OpenInNewWindow_DoesNotOpenTabs_ReturnsFileNames ()
+ {
+ // Arrange
+ var outcome = CreateOutcome(
+ ProjectLoadOutcome.LoadStatus.NeedsIntervention,
+ ["C:\\logs\\app.log"],
+ layoutXml: null);
+
+ var resolution = new MissingFilesResolution { OpenInNewWindow = true };
+
+ // Act
+ var result = _sut.ContinueLoad(outcome, resolution, restoreLayout: true);
+
+ // Assert
+ Assert.That(result.OpenedTabs, Is.False);
+ Assert.That(result.OpenInNewWindowFiles, Is.Not.Null);
+ Assert.That(result.OpenInNewWindowFiles, Has.Length.EqualTo(1));
+ Assert.That(_addFileTabCalls, Is.Empty, "OpenInNewWindow must not open tabs");
+ }
+
+ [Test]
+ public void ContinueLoad_OpenInNewWindowWithAlternatives_AppliesAlternativesThenResolves ()
+ {
+ // Arrange
+ var outcome = CreateOutcome(
+ ProjectLoadOutcome.LoadStatus.NeedsIntervention,
+ ["C:\\old\\missing.log", "C:\\logs\\existing.log"],
+ layoutXml: null);
+
+ var resolution = new MissingFilesResolution
+ {
+ OpenInNewWindow = true,
+ SelectedAlternatives = new Dictionary
+ {
+ ["C:\\old\\missing.log"] = "D:\\new\\found.log"
+ }
+ };
+
+ // Act
+ var result = _sut.ContinueLoad(outcome, resolution, restoreLayout: true);
+
+ // Assert
+ Assert.That(result.OpenInNewWindowFiles, Is.Not.Null);
+ Assert.That(result.OpenInNewWindowFiles, Has.Length.EqualTo(2));
+ // First file should be the alternative, second should be unchanged
+ // Exact values depend on PersisterHelpers.FindFilenameForSettings resolution
+ Assert.That(result.OpenInNewWindowFiles![0], Does.Contain("found.log"));
+ }
+
+ [Test]
+ public void ContinueLoad_UpdateSessionFileTrue_SavesAmendedProject ()
+ {
+ // Arrange: create a real temp .lxj so we can verify it was updated
+ var tempFile = CreateTempProjectFile(["C:\\old\\missing.log"], layoutXml: "");
+
+ try
+ {
+ var outcome = new ProjectLoadOutcome
+ {
+ Status = ProjectLoadOutcome.LoadStatus.NeedsIntervention,
+ ProjectData = new ProjectData
+ {
+ FileNames = ["C:\\old\\missing.log"],
+ TabLayoutXml = "",
+ ProjectFilePath = tempFile
+ },
+ LayoutXml = ""
+ };
+
+ var resolution = new MissingFilesResolution
+ {
+ UpdateSessionFile = true,
+ SelectedAlternatives = new Dictionary
+ {
+ ["C:\\old\\missing.log"] = "D:\\new\\found.log"
+ }
+ };
+
+ // Act
+ _ = _sut.ContinueLoad(outcome, resolution, restoreLayout: false);
+
+ // Assert: reload the file and check the path was updated
+ var reloaded = ProjectPersister.LoadProjectData(tempFile, _pluginRegistryMock.Object);
+ Assert.That(reloaded?.ProjectData?.FileNames, Has.Some.Contains("found.log"));
+ // Layout XML must be preserved
+ Assert.That(reloaded?.ProjectData?.TabLayoutXml, Is.EqualTo(""));
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ [Test]
+ public void ContinueLoad_UpdateSessionFileFalse_DoesNotSave ()
+ {
+ // Arrange
+ var tempFile = CreateTempProjectFile(["C:\\old\\missing.log"], layoutXml: null);
+
+ try
+ {
+ var originalContent = File.ReadAllText(tempFile);
+
+ var outcome = new ProjectLoadOutcome
+ {
+ Status = ProjectLoadOutcome.LoadStatus.NeedsIntervention,
+ ProjectData = new ProjectData
+ {
+ FileNames = ["C:\\old\\missing.log"],
+ ProjectFilePath = tempFile
+ }
+ };
+
+ var resolution = new MissingFilesResolution
+ {
+ UpdateSessionFile = false,
+ SelectedAlternatives = new Dictionary
+ {
+ ["C:\\old\\missing.log"] = "D:\\new\\found.log"
+ }
+ };
+
+ // Act
+ _ = _sut.ContinueLoad(outcome, resolution, restoreLayout: false);
+
+ // Assert: file content unchanged
+ Assert.That(File.ReadAllText(tempFile), Is.EqualTo(originalContent));
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ [Test]
+ public void ContinueLoad_NullResolution_SuccessReturnsCloseAllTabsFalse ()
+ {
+ // Arrange
+ var outcome = CreateSuccessOutcome(["C:\\logs\\app.log"], layoutXml: null);
+
+ // Act
+ var result = _sut.ContinueLoad(outcome, resolution: null, restoreLayout: true);
+
+ // Assert
+ Assert.That(result.CloseAllTabs, Is.False);
+ Assert.That(result.OpenInNewWindowFiles, Is.Null);
+ }
+
+ #endregion
+
+ #region SaveProject tests
+
+ [Test]
+ public void SaveProject_ValidData_ReturnsTrueAndNullError ()
+ {
+ // Arrange
+ var tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".lxj");
+ var projectData = new ProjectData
+ {
+ FileNames = ["C:\\logs\\app.log"],
+ TabLayoutXml = ""
+ };
+
+ try
+ {
+ // Act
+ var success = _sut.SaveProject(tempFile, projectData, out var errorMessage);
+
+ // Assert
+ Assert.That(success, Is.True);
+ Assert.That(errorMessage, Is.Null);
+ Assert.That(File.Exists(tempFile), Is.True);
+ }
+ finally
+ {
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+ }
+ }
+
+ [Test]
+ public void SaveProject_NullPath_ReturnsFalseWithErrorMessage ()
+ {
+ // Note: ProjectPersister.SaveProjectData swallows IOException/UnauthorizedAccessException/
+ // JsonSerializationException. Only uncaught exceptions (e.g. ArgumentNullException from
+ // File.WriteAllText(null,...)) propagate to ProjectFileHandler.SaveProject's catch block.
+ var projectData = new ProjectData
+ {
+ FileNames = ["C:\\logs\\app.log"]
+ };
+
+ // Act
+ var success = _sut.SaveProject(null!, projectData, out var errorMessage);
+
+ // Assert
+ Assert.That(success, Is.False);
+ Assert.That(errorMessage, Is.Not.Null.And.Not.Empty);
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private static ProjectLoadOutcome CreateSuccessOutcome (List fileNames, string? layoutXml)
+ {
+ return CreateOutcome(ProjectLoadOutcome.LoadStatus.Success, fileNames, layoutXml);
+ }
+
+ private static ProjectLoadOutcome CreateOutcome (ProjectLoadOutcome.LoadStatus status, List fileNames, string? layoutXml)
+ {
+ return new ProjectLoadOutcome
+ {
+ Status = status,
+ ProjectData = new ProjectData
+ {
+ FileNames = fileNames,
+ TabLayoutXml = layoutXml!
+ },
+ LayoutXml = layoutXml
+ };
+ }
+
+ ///
+ /// Creates a temporary .lxj project file with the given file names and optional layout XML. Returns the path to the
+ /// temp file.
+ ///
+ private static string CreateTempProjectFile (List fileNames, string? layoutXml)
+ {
+ var projectData = new ProjectData
+ {
+ FileNames = fileNames,
+ TabLayoutXml = layoutXml!
+ };
+
+ var tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".lxj");
+ ProjectPersister.SaveProjectData(tempFile, projectData);
+ return tempFile;
+ }
+
+ #endregion
+
+ public void Dispose ()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose (bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _stubLogWindow?.Dispose();
+ }
+
+ _disposed = true;
+ }
+}
\ No newline at end of file
diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
index 579e3507..0f6199d7 100644
--- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
+++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
@@ -24,6 +24,7 @@
using LogExpert.UI.Services.LedService;
using LogExpert.UI.Services.LogWindowCoordinatorService;
using LogExpert.UI.Services.MenuToolbarService;
+using LogExpert.UI.Services.ProjectFileHandlerService;
using LogExpert.UI.Services.TabControllerService;
using LogExpert.UI.Services.ToolWindowCoordinatorService;
@@ -51,6 +52,7 @@ internal partial class LogTabWindow : Form, ILogTabWindow
private readonly LogWindowCoordinator _logWindowCoordinator;
private readonly ToolWindowCoordinator _toolWindowCoordinator;
private readonly FileOperationService _fileOperationService;
+ private readonly ProjectFileHandler _projectFileHandler;
private bool _disposed;
@@ -110,6 +112,8 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu
_fileOperationService.FileHistoryChanged += (_, _) => FillHistoryMenu();
_fileOperationService.FileOpened += OnFileOperationServiceFileOpened;
+ _projectFileHandler = new ProjectFileHandler(PluginRegistry.PluginRegistry.Instance, request => _fileOperationService.AddFileTab(request));
+
_logWindowCoordinator = new LogWindowCoordinator(configManager, PluginRegistry.PluginRegistry.Instance, this, _tabController, _ledService, _fileOperationService);
//Fix MainMenu and externalToolsToolStrip.Location, if the location has been changed in the designer
@@ -1422,109 +1426,88 @@ private void CloseAllTabs ()
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0010:Add missing cases", Justification = "no need for the other switch cases")]
private void LoadProject (string projectFileName, bool restoreLayout)
{
- try
- {
- // Load project with validation
- var loadResult = ProjectPersister.LoadProjectData(projectFileName, PluginRegistry.PluginRegistry.Instance);
+ var outcome = _projectFileHandler.LoadProject(projectFileName);
+ bool openedTabs = false;
- if (loadResult?.ProjectData == null)
- {
- ShowOkMessage(
- Resources.LoadProject_UI_Message_Error_FileMaybeCorruptedOrInaccessible,
- Resources.LoadProject_UI_Message_Error_Title_ProjectLoadFailed,
- MessageBoxIcon.Error);
+ switch (outcome.Status)
+ {
+ case ProjectLoadOutcome.LoadStatus.Error:
+ {
+ ShowOkMessage(outcome.ErrorMessage ?? Resources.LogExpert_Common_UI_Title_Error,
+ Resources.LoadProject_UI_Message_Error_Title_ProjectLoadFailed,
+ MessageBoxIcon.Error);
+ return;
+ }
+ case ProjectLoadOutcome.LoadStatus.EmptyProject:
+ {
+ ShowOkMessage(outcome.ErrorMessage ?? Resources.LoadProject_UI_Message_Error_Title_SessionLoadFailed,
+ Resources.LoadProject_UI_Message_Message_FilesForSessionCouldNotBeFound,
+ MessageBoxIcon.Error);
+ return;
+ }
+ case ProjectLoadOutcome.LoadStatus.NeedsIntervention:
+ {
+ var (dialogResult, updateSessionFile, selectedAlternatives) =
+ MissingFilesDialog.ShowDialog(outcome.ValidationResult!, outcome.HasLayoutData);
- return;
- }
+ if (dialogResult == MissingFilesDialogResult.Cancel)
+ {
+ return;
+ }
- var projectData = loadResult.ProjectData;
- var hasLayoutData = projectData.TabLayoutXml != null;
+ if (dialogResult == MissingFilesDialogResult.IgnoreLayout)
+ {
+ restoreLayout = false;
+ }
- if (projectData.FileNames.Count == 0)
- {
- ShowOkMessage(
- Resources.LoadProject_UI_Message_Error_Title_SessionLoadFailed,
- Resources.LoadProject_UI_Message_Message_FilesForSessionCouldNotBeFound,
- MessageBoxIcon.Error);
- return;
- }
+ var resolution = new MissingFilesResolution
+ {
+ CloseAllTabs = dialogResult == MissingFilesDialogResult.CloseTabsAndRestoreLayout,
+ OpenInNewWindow = dialogResult == MissingFilesDialogResult.OpenInNewWindow,
+ UpdateSessionFile = updateSessionFile,
+ SelectedAlternatives = selectedAlternatives
+ };
- // Handle missing files or layout options
- if (loadResult.RequiresUserIntervention)
- {
- // Show enhanced dialog with browsing capability and layout options
- var (dialogResult, updateSessionFile, selectedAlternatives) = MissingFilesDialog.ShowDialog(loadResult.ValidationResult, hasLayoutData);
+ var interventionResult = _projectFileHandler.ContinueLoad(outcome, resolution, restoreLayout);
- if (dialogResult == MissingFilesDialogResult.Cancel)
- {
- return;
- }
+ if (updateSessionFile)
+ {
+ ShowOkMessage(Resources.LoadProject_UI_Message_Error_Message_UpdateSessionFile,
+ Resources.LoadProject_UI_Message_Error_Title_UpdateSessionFile,
+ MessageBoxIcon.Information);
+ }
- if (updateSessionFile)
- {
- // Replace original paths with selected alternatives in project data
- for (int i = 0; i < projectData.FileNames.Count; i++)
+ if (interventionResult.CloseAllTabs)
{
- var originalPath = projectData.FileNames[i];
- if (selectedAlternatives.TryGetValue(originalPath, out string value))
- {
- projectData.FileNames[i] = value;
- }
+ CloseAllTabs();
}
- ProjectPersister.SaveProjectData(projectFileName, projectData);
+ if (interventionResult.OpenInNewWindowFiles is not null)
+ {
+ LogExpertProxy.NewWindow([.. interventionResult.OpenInNewWindowFiles]);
+ return;
+ }
- ShowOkMessage(
- Resources.LoadProject_UI_Message_Error_Message_UpdateSessionFile,
- Resources.LoadProject_UI_Message_Error_Title_UpdateSessionFile,
- MessageBoxIcon.Information);
+ openedTabs = interventionResult.OpenedTabs;
+ break;
}
-
- // Handle layout-related results
- switch (dialogResult)
+ case ProjectLoadOutcome.LoadStatus.Success:
{
- case MissingFilesDialogResult.CloseTabsAndRestoreLayout:
- CloseAllTabs();
- break;
- case MissingFilesDialogResult.OpenInNewWindow:
- {
- var logFileNames = PersisterHelpers.FindFilenameForSettings(projectData.FileNames.AsReadOnly(), PluginRegistry.PluginRegistry.Instance);
- LogExpertProxy.NewWindow([.. logFileNames]);
- return;
- }
- case MissingFilesDialogResult.IgnoreLayout:
- hasLayoutData = false;
- break;
+ openedTabs = _projectFileHandler.ContinueLoad(outcome, null, restoreLayout).OpenedTabs;
+ break;
}
- }
-
- foreach (var fileName in projectData.FileNames)
- {
- _ = hasLayoutData
- ? _fileOperationService.AddFileTabDeferred(fileName, false, null, true, null)
- : _fileOperationService.AddFileTab(new FileTabRequest { FileName = fileName, ForcePersistenceLoading = true });
- }
+ }
- // Restore layout only if we loaded at least one file
- if (hasLayoutData && restoreLayout && _tabController.GetWindowCount() > 0)
- {
- _logger.Info("Restoring layout");
- // Re-creating tool (non-document) windows is needed because the DockPanel control would throw strange errors
- DestroyBookmarkWindow();
- InitToolWindows();
- RestoreLayout(projectData.TabLayoutXml);
- }
- else if (_tabController.GetWindowCount() == 0)
- {
- _logger.Warn("No files loaded, skipping layout restoration");
- }
+ if (restoreLayout && outcome.HasLayoutData && openedTabs)
+ {
+ _logger.Info("Restoring layout");
+ DestroyBookmarkWindow();
+ InitToolWindows();
+ RestoreLayout(outcome.LayoutXml!);
}
- catch (Exception ex)
+ else if (!openedTabs)
{
- ShowOkMessage(
- $"Error loading project: {ex.Message}",
- Resources.LogExpert_Common_UI_Title_Error,
- MessageBoxIcon.Error);
+ _logger.Warn("No files loaded, skipping layout restoration");
}
}
@@ -2396,7 +2379,12 @@ private void OnSaveProjectToolStripMenuItemClick (object sender, EventArgs e)
TabLayoutXml = SaveLayout()
};
- ProjectPersister.SaveProjectData(fileName, projectData);
+ if (!_projectFileHandler.SaveProject(fileName, projectData, out var errorMessage))
+ {
+ ShowOkMessage(errorMessage ?? Resources.LogExpert_Common_UI_Title_Error,
+ Resources.LogExpert_Common_UI_Title_Error,
+ MessageBoxIcon.Error);
+ }
}
}
diff --git a/src/LogExpert.UI/Interface/IProjectFileHandler.cs b/src/LogExpert.UI/Interface/IProjectFileHandler.cs
new file mode 100644
index 00000000..dff9d966
--- /dev/null
+++ b/src/LogExpert.UI/Interface/IProjectFileHandler.cs
@@ -0,0 +1,13 @@
+using LogExpert.Core.Classes.Persister;
+using LogExpert.UI.Services.ProjectFileHandlerService;
+
+namespace LogExpert.UI.Interface;
+
+internal interface IProjectFileHandler
+{
+ ProjectLoadOutcome LoadProject (string projectFileName);
+
+ ContinueLoadResult ContinueLoad (ProjectLoadOutcome loadOutcome, MissingFilesResolution? resolution, bool restoreLayout);
+
+ bool SaveProject (string projectFileName, ProjectData projectData, out string? errorMessage);
+}
\ No newline at end of file
diff --git a/src/LogExpert.UI/Services/ProjectFileHandlerService/ContinueLoadResult.cs b/src/LogExpert.UI/Services/ProjectFileHandlerService/ContinueLoadResult.cs
new file mode 100644
index 00000000..ee71c88a
--- /dev/null
+++ b/src/LogExpert.UI/Services/ProjectFileHandlerService/ContinueLoadResult.cs
@@ -0,0 +1,10 @@
+namespace LogExpert.UI.Services.ProjectFileHandlerService;
+
+internal readonly record struct ContinueLoadResult
+{
+ public bool OpenedTabs { get; init; }
+
+ public bool CloseAllTabs { get; init; }
+
+ public string[]? OpenInNewWindowFiles { get; init; }
+}
\ No newline at end of file
diff --git a/src/LogExpert.UI/Services/ProjectFileHandlerService/MissingFilesResolution.cs b/src/LogExpert.UI/Services/ProjectFileHandlerService/MissingFilesResolution.cs
new file mode 100644
index 00000000..0a14b1b3
--- /dev/null
+++ b/src/LogExpert.UI/Services/ProjectFileHandlerService/MissingFilesResolution.cs
@@ -0,0 +1,12 @@
+namespace LogExpert.UI.Services.ProjectFileHandlerService;
+
+internal readonly record struct MissingFilesResolution
+{
+ public bool CloseAllTabs { get; init; }
+
+ public bool OpenInNewWindow { get; init; }
+
+ public bool UpdateSessionFile { get; init; }
+
+ public IReadOnlyDictionary? SelectedAlternatives { get; init; }
+}
\ No newline at end of file
diff --git a/src/LogExpert.UI/Services/ProjectFileHandlerService/ProjectFileHandler.cs b/src/LogExpert.UI/Services/ProjectFileHandlerService/ProjectFileHandler.cs
new file mode 100644
index 00000000..afa01c4b
--- /dev/null
+++ b/src/LogExpert.UI/Services/ProjectFileHandlerService/ProjectFileHandler.cs
@@ -0,0 +1,186 @@
+using System.Runtime.Versioning;
+
+using LogExpert.Core.Classes.Persister;
+using LogExpert.Core.Interfaces;
+using LogExpert.UI.Controls.LogWindow;
+using LogExpert.UI.Interface;
+using LogExpert.UI.Services.FileOperationService;
+
+using NLog;
+
+namespace LogExpert.UI.Services.ProjectFileHandlerService;
+
+[SupportedOSPlatform("windows")]
+internal sealed class ProjectFileHandler (
+ IPluginRegistry pluginRegistry,
+ Func addFileTab)
+ : IProjectFileHandler
+{
+ private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
+ public ProjectLoadOutcome LoadProject (string projectFileName)
+ {
+ try
+ {
+ if (!File.Exists(projectFileName))
+ {
+ _logger.Warn("LoadProject: File does not exist: {FileName}", projectFileName);
+
+ return new ProjectLoadOutcome
+ {
+ Status = ProjectLoadOutcome.LoadStatus.Error,
+ ErrorMessage = $"Project file not found: {projectFileName}"
+ };
+ }
+
+ var loadResult = ProjectPersister.LoadProjectData(projectFileName, pluginRegistry);
+
+ if (loadResult?.ProjectData == null)
+ {
+ _logger.Warn("LoadProject: ProjectData is null for {FileName}", projectFileName);
+
+ return new ProjectLoadOutcome
+ {
+ Status = ProjectLoadOutcome.LoadStatus.Error,
+ ErrorMessage = Resources.LoadProject_UI_Message_Error_FileMaybeCorruptedOrInaccessible
+ };
+ }
+
+ var projectData = loadResult.ProjectData;
+
+ if (projectData.FileNames.Count == 0)
+ {
+ _logger.Warn("LoadProject: No files in project {FileName}", projectFileName);
+
+ return new ProjectLoadOutcome
+ {
+ Status = ProjectLoadOutcome.LoadStatus.EmptyProject,
+ ErrorMessage = Resources.LoadProject_UI_Message_Message_FilesForSessionCouldNotBeFound
+ };
+ }
+
+ var layoutXml = projectData.TabLayoutXml;
+
+ return loadResult.RequiresUserIntervention
+ ? new ProjectLoadOutcome
+ {
+ Status = ProjectLoadOutcome.LoadStatus.NeedsIntervention,
+ ProjectData = projectData,
+ ValidationResult = loadResult.ValidationResult,
+ LayoutXml = layoutXml
+ }
+ : new ProjectLoadOutcome
+ {
+ Status = ProjectLoadOutcome.LoadStatus.Success,
+ ProjectData = projectData,
+ LayoutXml = layoutXml
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "LoadProject: Exception loading {FileName}", projectFileName);
+
+ return new ProjectLoadOutcome
+ {
+ Status = ProjectLoadOutcome.LoadStatus.Error,
+ ErrorMessage = $"Error loading project: {ex.Message}"
+ };
+ }
+ }
+
+ public ContinueLoadResult ContinueLoad (ProjectLoadOutcome loadOutcome, MissingFilesResolution? resolution, bool restoreLayout)
+ {
+ ArgumentNullException.ThrowIfNull(loadOutcome);
+ ArgumentNullException.ThrowIfNull(loadOutcome.ProjectData);
+
+ var projectData = loadOutcome.ProjectData;
+
+ // Apply selected alternatives to file paths (in-place, exact string match)
+ if (resolution?.SelectedAlternatives is { } alternatives)
+ {
+ for (int i = 0; i < projectData.FileNames.Count; i++)
+ {
+ var originalPath = projectData.FileNames[i];
+ if (alternatives.TryGetValue(originalPath, out var replacement))
+ {
+ projectData.FileNames[i] = replacement;
+ }
+ }
+ }
+
+ // Save updated session file if requested
+ if (resolution is { UpdateSessionFile: true } && !string.IsNullOrEmpty(projectData.ProjectFilePath))
+ {
+ try
+ {
+ ProjectPersister.SaveProjectData(projectData.ProjectFilePath, projectData);
+ _logger.Info("ContinueLoad: Updated session file {FileName}", projectData.ProjectFilePath);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "ContinueLoad: Failed to update session file {FileName}", projectData.ProjectFilePath);
+ }
+ }
+
+ // Handle "Open in new window" — resolve file names but do NOT open tabs
+ if (resolution is { OpenInNewWindow: true })
+ {
+ var resolvedFiles = PersisterHelpers.FindFilenameForSettings(
+ projectData.FileNames.AsReadOnly(), pluginRegistry);
+
+ return new ContinueLoadResult
+ {
+ OpenedTabs = false,
+ CloseAllTabs = false,
+ OpenInNewWindowFiles = [.. resolvedFiles]
+ };
+ }
+
+ // Open file tabs
+ bool deferForLayout = loadOutcome.HasLayoutData && restoreLayout;
+ int openedCount = 0;
+
+ foreach (var fileName in projectData.FileNames)
+ {
+ var request = new FileTabRequest
+ {
+ FileName = fileName,
+ ForcePersistenceLoading = true,
+ DoNotAddToDockPanel = deferForLayout
+ };
+
+ try
+ {
+ _ = addFileTab(request);
+ openedCount++;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "ContinueLoad: Failed to open tab for {FileName}", fileName);
+ }
+ }
+
+ return new ContinueLoadResult
+ {
+ OpenedTabs = openedCount > 0,
+ CloseAllTabs = resolution?.CloseAllTabs ?? false,
+ OpenInNewWindowFiles = null
+ };
+ }
+
+ public bool SaveProject (string projectFileName, ProjectData projectData, out string? errorMessage)
+ {
+ try
+ {
+ ProjectPersister.SaveProjectData(projectFileName, projectData);
+ errorMessage = null;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SaveProject: Failed to save {FileName}", projectFileName);
+ errorMessage = $"Error saving project: {ex.Message}";
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LogExpert.UI/Services/ProjectFileHandlerService/ProjectLoadOutcome.cs b/src/LogExpert.UI/Services/ProjectFileHandlerService/ProjectLoadOutcome.cs
new file mode 100644
index 00000000..d1e352e7
--- /dev/null
+++ b/src/LogExpert.UI/Services/ProjectFileHandlerService/ProjectLoadOutcome.cs
@@ -0,0 +1,26 @@
+using LogExpert.Core.Classes.Persister;
+
+namespace LogExpert.UI.Services.ProjectFileHandlerService;
+
+internal sealed class ProjectLoadOutcome
+{
+ public enum LoadStatus
+ {
+ Success,
+ NeedsIntervention,
+ Error,
+ EmptyProject
+ }
+
+ public required LoadStatus Status { get; init; }
+
+ public ProjectData? ProjectData { get; init; }
+
+ public ProjectValidationResult? ValidationResult { get; init; }
+
+ public string? LayoutXml { get; init; }
+
+ public bool HasLayoutData => !string.IsNullOrEmpty(LayoutXml);
+
+ public string? ErrorMessage { get; init; }
+}
\ No newline at end of file
diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs
index 332bbcc0..f0a0f07a 100644
--- a/src/PluginRegistry/PluginHashGenerator.Generated.cs
+++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs
@@ -10,7 +10,7 @@ public static partial class PluginValidator
{
///
/// Gets pre-calculated SHA256 hashes for built-in plugins.
- /// Generated: 2026-05-06 14:47:19 UTC
+ /// Generated: 2026-05-07 06:58:46 UTC
/// Configuration: Release
/// Plugin count: 22
///
@@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes()
{
return new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- ["AutoColumnizer.dll"] = "FD61116D4A97900F68D049775D265469B9C6B3202035EA8B66084AE1944D5CF3",
+ ["AutoColumnizer.dll"] = "DF22E41B38A18AAE704D35248357C32D8CC9476FE69364BFE009931E2EFA704A",
["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
- ["CsvColumnizer.dll"] = "5AEFECB027955EF4EB9274553F94081F17DD6457AAD57AF664AFFBD3932889CF",
- ["CsvColumnizer.dll (x86)"] = "5AEFECB027955EF4EB9274553F94081F17DD6457AAD57AF664AFFBD3932889CF",
- ["DefaultPlugins.dll"] = "0F7CE41008BBBD1BCDF4066C07D1F7D3B4DFEC9FFBF75CDFAE29E93E4EF994DC",
- ["FlashIconHighlighter.dll"] = "C9889BE6722A38B9A58E7A1796985D68A24189A7D1EDAA7C5F929D85F8FC9BA7",
- ["GlassfishColumnizer.dll"] = "AD4AE9B53F54D9E37B9F5C25FD8C1BF764FB58EE8DBF29842791552B7F414899",
- ["JsonColumnizer.dll"] = "22777672274FD32FC36E4AEF7BA3EDBBC80C15B104E7E03F225AA21E485296A8",
- ["JsonCompactColumnizer.dll"] = "8817DAFF4E5A574FC28D1099498E32BC7151C3FD5557BD9E8B955D633C2812A6",
- ["Log4jXmlColumnizer.dll"] = "2A215605B7E093C83424AFCDA440BDD23C6732536B08AEA1EF081D1B540A48D4",
- ["LogExpert.Core.dll"] = "AA2562DBEBC5508B119C79BB84D0F0B441704B15725E870F4802E8B319CBE613",
- ["LogExpert.Resources.dll"] = "90CDAA06B3B2A9E7C521016EE3610FE3A0E8F71C0DC7BFE5DCEBC32BD866FE70",
+ ["CsvColumnizer.dll"] = "89D655FF1D95A41B71DFCF22772A5C2DD514D0D8688D825C2AB34D66AD7A5CCD",
+ ["CsvColumnizer.dll (x86)"] = "89D655FF1D95A41B71DFCF22772A5C2DD514D0D8688D825C2AB34D66AD7A5CCD",
+ ["DefaultPlugins.dll"] = "20D6D3D6F24992625285EBE2698B1E4EEB3018B8C0FE3A6221AA671ED90FAAF3",
+ ["FlashIconHighlighter.dll"] = "53B698C98953F7D6396698270A5A05C7C80BD66ED6A44CE3EE0F07C277384E68",
+ ["GlassfishColumnizer.dll"] = "C5C19B0C4F68ED8363E2281B512D3FCCCEC7C7C6D3A4F31A20F13E40DADC2373",
+ ["JsonColumnizer.dll"] = "F0E9425822F890544198B5A016A1E325F4F38F81DAA7F026DFD8169D51B6E9AA",
+ ["JsonCompactColumnizer.dll"] = "B621954BC540B4FF6B39EA15A7130EEBF357B5EE1747D74E495D9D5F5C59C928",
+ ["Log4jXmlColumnizer.dll"] = "DB20826D20DF0827422E1E24EBF117CFCEB26F964B194FF065F3B31EA9BDDB69",
+ ["LogExpert.Core.dll"] = "2567E08691D61D95BDDFF8326D922480BC2A6CB71A7B482FE2AD9E76D764CDDD",
+ ["LogExpert.Resources.dll"] = "016630BC957D4B63CD6B06C7C6B64F68F2BDC8A355C9357BBCF7E28BEAD89AA3",
["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
- ["RegexColumnizer.dll"] = "237C573356EA01DAA956375E9E9D6EA477C45BCB81E3F1E7035595AEBA558B7D",
- ["SftpFileSystem.dll"] = "B34DC1E5CE32D6C5D0E4522A358471D451B455835D318400327122139601BA6E",
- ["SftpFileSystem.dll (x86)"] = "AB972F0E77DA7706214B042908AF40BE3BBB3EA12123AD76946EA224061E8C27",
- ["SftpFileSystem.Resources.dll"] = "D7F1419A2F25058D937309DECC8A02615F2A150B219A75D51667001124A32075",
- ["SftpFileSystem.Resources.dll (x86)"] = "D7F1419A2F25058D937309DECC8A02615F2A150B219A75D51667001124A32075",
+ ["RegexColumnizer.dll"] = "1479ECA203FAEF95B4FC68484BE712C4B21448CE56E6BBC7BD0B709D672C3770",
+ ["SftpFileSystem.dll"] = "16027F1015B57E89E0A9EB9E7427813FC0528A81BA9F98E233E932E30F06310E",
+ ["SftpFileSystem.dll (x86)"] = "E9EDB8FBED9DE17C3C3B1417C54ABBEF6B590C0AABC881C15D5F0D4157C1DC11",
+ ["SftpFileSystem.Resources.dll"] = "5E9E3C2251A932E34C2106502E0386CB927D4DB8C9E2F9D994E2DC1ED7C757CC",
+ ["SftpFileSystem.Resources.dll (x86)"] = "5E9E3C2251A932E34C2106502E0386CB927D4DB8C9E2F9D994E2DC1ED7C757CC",
};
}