diff --git a/src/LogExpert.Tests/LogExpert.Tests.csproj b/src/LogExpert.Tests/LogExpert.Tests.csproj
index f35a6ca8..271b8f26 100644
--- a/src/LogExpert.Tests/LogExpert.Tests.csproj
+++ b/src/LogExpert.Tests/LogExpert.Tests.csproj
@@ -33,13 +33,13 @@
-
+
PreserveNewest
-
+
PreserveNewest
-
+
PreserveNewest
diff --git a/src/LogExpert.Tests/Services/SessionHandlerTests.cs b/src/LogExpert.Tests/Services/SessionHandlerTests.cs
index 5153cb43..d2c30616 100644
--- a/src/LogExpert.Tests/Services/SessionHandlerTests.cs
+++ b/src/LogExpert.Tests/Services/SessionHandlerTests.cs
@@ -21,6 +21,8 @@ internal class SessionHandlerTests : IDisposable
{
private Mock _pluginRegistryMock = null!;
private List _addFileTabCalls = null!;
+ private List _callOrder = null!;
+ private int _closeAllTabsCallCount;
private Settings _settings;
private Mock _configManagerMock;
private LogWindow _stubLogWindow = null!;
@@ -38,6 +40,8 @@ public void SetUp ()
_pluginRegistryMock = new Mock();
_configManagerMock = new Mock();
_addFileTabCalls = [];
+ _callOrder = [];
+ _closeAllTabsCallCount = 0;
_settings = new Settings();
_ = _configManagerMock.Setup(cm => cm.Settings).Returns(_settings);
@@ -59,7 +63,13 @@ public void SetUp ()
request =>
{
_addFileTabCalls.Add(request);
+ _callOrder.Add($"addtab:{request.FileName}");
return _stubLogWindow;
+ },
+ () =>
+ {
+ _closeAllTabsCallCount++;
+ _callOrder.Add("close");
});
}
@@ -364,9 +374,8 @@ public void ContinueLoad_DuplicateFileWithAlternative_ReplacesBothOccurrences ()
}
[Test]
- public void ContinueLoad_CloseAllTabs_ReturnsTrueInResult ()
+ public void ContinueLoad_CloseAllTabs_ClosesExistingTabsBeforeOpeningNewWindows ()
{
- // Arrange
var outcome = CreateOutcome(
SessionLoadOutcome.LoadStatus.NeedsIntervention,
["C:\\logs\\app.log"],
@@ -378,7 +387,9 @@ public void ContinueLoad_CloseAllTabs_ReturnsTrueInResult ()
var result = _sut.ContinueLoad(outcome, resolution, restoreLayout: true);
// Assert
- Assert.That(result.CloseAllTabs, Is.True);
+ Assert.That(_closeAllTabsCallCount, Is.EqualTo(1), "close must be invoked exactly once");
+ Assert.That(_callOrder, Is.EqualTo(new[] { "close", "addtab:C:\\logs\\app.log" }),
+ "existing tabs must be closed before the new windows are opened");
Assert.That(result.OpenedTabs, Is.True);
Assert.That(result.OpenInNewWindowFiles, Is.Null);
}
@@ -402,6 +413,7 @@ public void ContinueLoad_OpenInNewWindow_DoesNotOpenTabs_ReturnsFileNames ()
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");
+ Assert.That(_closeAllTabsCallCount, Is.EqualTo(0), "OpenInNewWindow must not close existing tabs");
}
[Test]
@@ -519,7 +531,7 @@ public void ContinueLoad_UpdateSessionFileFalse_DoesNotSave ()
}
[Test]
- public void ContinueLoad_NullResolution_SuccessReturnsCloseAllTabsFalse ()
+ public void ContinueLoad_NullResolution_DoesNotCloseExistingTabs ()
{
// Arrange
var outcome = CreateSuccessOutcome(["C:\\logs\\app.log"], layoutXml: null);
@@ -528,10 +540,29 @@ public void ContinueLoad_NullResolution_SuccessReturnsCloseAllTabsFalse ()
var result = _sut.ContinueLoad(outcome, resolution: null, restoreLayout: true);
// Assert
- Assert.That(result.CloseAllTabs, Is.False);
+ Assert.That(_closeAllTabsCallCount, Is.EqualTo(0), "existing tabs must not be closed when no resolution is given");
Assert.That(result.OpenInNewWindowFiles, Is.Null);
}
+ [Test]
+ public void ContinueLoad_CloseAllTabsFalse_DoesNotCloseExistingTabs ()
+ {
+ // Arrange
+ var outcome = CreateOutcome(
+ SessionLoadOutcome.LoadStatus.NeedsIntervention,
+ ["C:\\logs\\app.log"],
+ layoutXml: "");
+
+ var resolution = new MissingFilesResolution { CloseAllTabs = false };
+
+ // Act
+ _ = _sut.ContinueLoad(outcome, resolution, restoreLayout: true);
+
+ // Assert
+ Assert.That(_closeAllTabsCallCount, Is.EqualTo(0));
+ Assert.That(_addFileTabCalls, Has.Count.EqualTo(1));
+ }
+
#endregion
#region SaveSession tests
diff --git a/src/LogExpert.Tests/Data/50 MB UTF16.txt b/src/LogExpert.Tests/TestData/50 MB UTF16.txt
similarity index 100%
rename from src/LogExpert.Tests/Data/50 MB UTF16.txt
rename to src/LogExpert.Tests/TestData/50 MB UTF16.txt
diff --git a/src/LogExpert.Tests/Data/50 MB UTF8.txt b/src/LogExpert.Tests/TestData/50 MB UTF8.txt
similarity index 100%
rename from src/LogExpert.Tests/Data/50 MB UTF8.txt
rename to src/LogExpert.Tests/TestData/50 MB UTF8.txt
diff --git a/src/LogExpert.Tests/Data/50 MB.txt b/src/LogExpert.Tests/TestData/50 MB.txt
similarity index 100%
rename from src/LogExpert.Tests/Data/50 MB.txt
rename to src/LogExpert.Tests/TestData/50 MB.txt
diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
index 30de7a99..7fa3260d 100644
--- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
+++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
@@ -8,7 +8,6 @@
using ColumnizerLib;
using LogExpert.Core.Classes;
-using LogExpert.Core.Classes.Columnizer;
using LogExpert.Core.Classes.Persister;
using LogExpert.Core.Config;
using LogExpert.Core.Entities;
@@ -115,7 +114,7 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu
_fileOperationService.FileHistoryChanged += (_, _) => FillHistoryMenu();
_fileOperationService.FileOpened += OnFileOperationServiceFileOpened;
- _sessionHandler = new SessionHandler(PluginRegistry.PluginRegistry.Instance, request => _fileOperationService.AddFileTab(request));
+ _sessionHandler = new SessionHandler(PluginRegistry.PluginRegistry.Instance, request => _fileOperationService.AddFileTab(request), CloseAllTabs);
_toolLaunchService = new ToolLaunchService(PluginRegistry.PluginRegistry.Instance);
_logWindowCoordinator = new LogWindowCoordinator(configManager, PluginRegistry.PluginRegistry.Instance, this, _tabController, _ledService, _fileOperationService);
@@ -1460,6 +1459,8 @@ private void LoadSession (string sessionFileName, bool restoreLayout)
SelectedAlternatives = selectedAlternatives
};
+ // ContinueLoad closes existing tabs (when requested) before opening the
+ // new session windows, so no post-hoc CloseAllTabs() is needed here.
var interventionResult = _sessionHandler.ContinueLoad(outcome, resolution, restoreLayout);
if (updateSessionFile)
@@ -1469,11 +1470,6 @@ private void LoadSession (string sessionFileName, bool restoreLayout)
MessageBoxIcon.Information);
}
- if (interventionResult.CloseAllTabs)
- {
- CloseAllTabs();
- }
-
if (interventionResult.OpenInNewWindowFiles is not null)
{
LogExpertProxy.NewWindow([.. interventionResult.OpenInNewWindowFiles]);
diff --git a/src/LogExpert.UI/Services/SessionHandlerService/ContinueLoadResult.cs b/src/LogExpert.UI/Services/SessionHandlerService/ContinueLoadResult.cs
index 250eaa02..c3ff3c8f 100644
--- a/src/LogExpert.UI/Services/SessionHandlerService/ContinueLoadResult.cs
+++ b/src/LogExpert.UI/Services/SessionHandlerService/ContinueLoadResult.cs
@@ -4,7 +4,5 @@ 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/SessionHandlerService/SessionHandler.cs b/src/LogExpert.UI/Services/SessionHandlerService/SessionHandler.cs
index 2178b37c..2185a601 100644
--- a/src/LogExpert.UI/Services/SessionHandlerService/SessionHandler.cs
+++ b/src/LogExpert.UI/Services/SessionHandlerService/SessionHandler.cs
@@ -13,7 +13,8 @@ namespace LogExpert.UI.Services.SessionHandlerService;
[SupportedOSPlatform("windows")]
internal sealed class SessionHandler (
IPluginRegistry pluginRegistry,
- Func addFileTab)
+ Func addFileTab,
+ Action closeAllTabs)
: ISessionHandler
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
@@ -137,11 +138,19 @@ InvalidOperationException or
return new ContinueLoadResult
{
OpenedTabs = false,
- CloseAllTabs = false,
OpenInNewWindowFiles = [.. resolvedFiles]
};
}
+ // Close existing tabs BEFORE opening the new session windows. The new windows are
+ // tracked by the tab controller as soon as they are created (even deferred ones that
+ // are not yet on the dock panel), so closing afterwards would also close them and the
+ // subsequent layout restore would find nothing to dock.
+ if (resolution is { CloseAllTabs: true })
+ {
+ closeAllTabs();
+ }
+
// Open file tabs
bool deferForLayout = loadOutcome.HasLayoutData && restoreLayout;
int openedCount = 0;
@@ -176,7 +185,6 @@ and not InvalidProgramException
return new ContinueLoadResult
{
OpenedTabs = openedCount > 0,
- CloseAllTabs = resolution?.CloseAllTabs ?? false,
OpenInNewWindowFiles = null
};
}
diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs
index 07228d8c..15809db5 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-06-15 07:15:17 UTC
+ /// Generated: 2026-06-17 13:20:19 UTC
/// Configuration: Release
/// Plugin count: 21
///
@@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes()
{
return new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- ["AutoColumnizer.dll"] = "4180178F5A56D7F66E8A975FF7F780C7A7812F08160A892A3CE26E4ED1EFD0DC",
+ ["AutoColumnizer.dll"] = "61E445A9F8960311A1EF0A21001B121DCAFFFDD2243ACE04BCF840560B372EA5",
["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
- ["CsvColumnizer.dll"] = "703EF513A4DC02B4B69707DE237025AF4A87B0506A850D5231BCCB8CD6322ED1",
- ["CsvColumnizer.dll (x86)"] = "703EF513A4DC02B4B69707DE237025AF4A87B0506A850D5231BCCB8CD6322ED1",
- ["DefaultPlugins.dll"] = "339862337AACE8645B70F6E75F5FA4E875A6164FF055E9E8171E8709A650BA90",
- ["FlashIconHighlighter.dll"] = "1B1697E0EB2416F656BD6120A2C64EBF06260F8B1346CDB851979B23AF8F8444",
- ["GlassfishColumnizer.dll"] = "327AC39EEA2E04F055411644AED8D8603F760AF76BF9913E8ED4FA0211E84D73",
- ["JsonColumnizer.dll"] = "8EA699F6C34300A43228375ED6590C682299C5577E164DF186019785A7F37F79",
- ["JsonCompactColumnizer.dll"] = "174FCD6677405DC65AC3823D12042F9EFCB456B55F5457572A10C2E48A9D0F81",
- ["Log4jXmlColumnizer.dll"] = "0AB084A6EF69717EF9DE7294D68F6F5D149815AA5828FDA93AEB1FD88EA9D7A1",
- ["LogExpert.Resources.dll"] = "CF23AE53827FCDA8430ECF6E20F48B03D5BA86975F25747D5E4B380A946584A1",
+ ["CsvColumnizer.dll"] = "9DE6D6CA21188746BAE404F5EB5D4B3B0897A50ACD596994FE730033DE18DEBA",
+ ["CsvColumnizer.dll (x86)"] = "9DE6D6CA21188746BAE404F5EB5D4B3B0897A50ACD596994FE730033DE18DEBA",
+ ["DefaultPlugins.dll"] = "24DFE5E3B38360ADE7EB0C23DEB15AC98F2649CDC517C1E5A8E3D5666DDC1AB1",
+ ["FlashIconHighlighter.dll"] = "2F196A6B52D1ED5FAC921BC487E27A1DBA444667A76E55C912C145D49CA9CE85",
+ ["GlassfishColumnizer.dll"] = "EFAE70B8767B0C23F0EB72495968F4BEC6E0C1CE0E55484C15DA9DAFE97A0B74",
+ ["JsonColumnizer.dll"] = "5C5D99331694A0543750FD78B35CA3FF082949A45AC731FB06DE8B505F6CA2F5",
+ ["JsonCompactColumnizer.dll"] = "05386F718D6ECBAD1C788B55E70DC8888B8F8C7069702841B081B6CA9D7A54AC",
+ ["Log4jXmlColumnizer.dll"] = "24D85F8884DED3345E25A6592B4808771A6A77B005EF2EAAA05FB76756C36D8B",
+ ["LogExpert.Resources.dll"] = "F94CC3F814A338588EE3E92E4B4D559EDF7E2CF7554A6CDD95C1E0BA6FBCA613",
["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"] = "E67FCF12B4563F1C10E46571815D3AF952D9BBD9DA46215812A7FD31D753C8F5",
- ["SftpFileSystem.dll"] = "014CD6FC3EF2A405033C0AB96D90CAB54B0229332F4DCF8176E2F43C1528F487",
- ["SftpFileSystem.dll (x86)"] = "E23DDE1464E95D4621688655D9ABD761F0CCB221A5962B97D489E2B5A20BB87F",
- ["SftpFileSystem.Resources.dll"] = "0155F98A60C39252C0006A1244A8BFD987DAC03406A3B3BF5D1E012C52FF2A48",
- ["SftpFileSystem.Resources.dll (x86)"] = "0155F98A60C39252C0006A1244A8BFD987DAC03406A3B3BF5D1E012C52FF2A48",
+ ["RegexColumnizer.dll"] = "1FD933319BA6978B1E78444472BE7540EC76576327BB09C7900BE66320CAC2EF",
+ ["SftpFileSystem.dll"] = "49D9DB34840774486F3944B49B165279E68A8F8A43C0E93E5DE6D41F9317C1D2",
+ ["SftpFileSystem.dll (x86)"] = "EEAA1B42AE124ECDF0FF63A80C00455FB63778588B656822D366B4D4F5180CC7",
+ ["SftpFileSystem.Resources.dll"] = "6FFC5A1EECA8363E97F7235DD70EE2E9F91870C0BD45E9A3A17669D7BD38423D",
+ ["SftpFileSystem.Resources.dll (x86)"] = "6FFC5A1EECA8363E97F7235DD70EE2E9F91870C0BD45E9A3A17669D7BD38423D",
};
}