diff --git a/src/LogExpert.Tests/Services/ToolLaunchServiceTests.cs b/src/LogExpert.Tests/Services/ToolLaunchServiceTests.cs new file mode 100644 index 00000000..1eb89663 --- /dev/null +++ b/src/LogExpert.Tests/Services/ToolLaunchServiceTests.cs @@ -0,0 +1,62 @@ +using System.Runtime.Versioning; + +using LogExpert.Core.Interfaces; +using LogExpert.UI.Services.ToolLaunchService; + +using Moq; + +using NUnit.Framework; + +namespace LogExpert.Tests.Services; + +[TestFixture] +[Apartment(ApartmentState.STA)] +[SupportedOSPlatform("windows")] +internal class ToolLaunchServiceTests +{ + private Mock _pluginRegistryMock = null!; + private ToolLaunchService _sut = null!; + + [SetUp] + public void SetUp () + { + _pluginRegistryMock = new Mock(); + _ = _pluginRegistryMock.Setup(pr => pr.RegisteredColumnizers).Returns([]); + + _sut = new ToolLaunchService(_pluginRegistryMock.Object); + } + + [Test] + public void Launch_WithEmptyCmd_ReturnsHasErrorTrue () + { + var request = new ToolLaunchRequest { Cmd = string.Empty, Args = string.Empty, SysoutPipe = false }; + + var result = _sut.Launch(request); + + Assert.That(result.HasError, Is.True); + Assert.That(result.ErrorMessage, Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Launch_WithValidCmdAndNoSysoutPipe_ReturnsSuccessWithNullPipeFileName () + { + var request = new ToolLaunchRequest { Cmd = "cmd.exe", Args = "/c exit 0", SysoutPipe = false }; + + var result = _sut.Launch(request); + + Assert.That(result.HasError, Is.False); + Assert.That(result.PipeFileName, Is.Null); + } + + [Test] + public void Launch_WithValidCmdAndSysoutPipe_ReturnsPipeFileNamePointingToExistingFile () + { + var request = new ToolLaunchRequest { Cmd = "cmd.exe", Args = "/c echo hello", SysoutPipe = true }; + + var result = _sut.Launch(request); + + Assert.That(result.HasError, Is.False); + Assert.That(result.PipeFileName, Is.Not.Null.And.Not.Empty); + Assert.That(File.Exists(result.PipeFileName), Is.True); + } +} diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 52466129..f2c142e0 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -27,6 +27,7 @@ using LogExpert.UI.Services.MenuToolbarService; using LogExpert.UI.Services.SessionHandlerService; using LogExpert.UI.Services.TabControllerService; +using LogExpert.UI.Services.ToolLaunchService; using LogExpert.UI.Services.ToolWindowCoordinatorService; using NLog; @@ -54,6 +55,7 @@ internal partial class LogTabWindow : Form, ILogTabWindow private readonly ToolWindowCoordinator _toolWindowCoordinator; private readonly FileOperationService _fileOperationService; private readonly SessionHandler _sessionHandler; + private readonly ToolLaunchService _toolLaunchService; private bool _disposed; @@ -114,6 +116,7 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu _fileOperationService.FileOpened += OnFileOperationServiceFileOpened; _sessionHandler = new SessionHandler(PluginRegistry.PluginRegistry.Instance, request => _fileOperationService.AddFileTab(request)); + _toolLaunchService = new ToolLaunchService(PluginRegistry.PluginRegistry.Instance); _logWindowCoordinator = new LogWindowCoordinator(configManager, PluginRegistry.PluginRegistry.Instance, this, _tabController, _ledService, _fileOperationService); @@ -1328,102 +1331,69 @@ private void ToolButtonClick (ToolEntry toolEntry) return; } + ToolLaunchRequest request; + if (CurrentLogWindow != null) { var line = CurrentLogWindow.GetCurrentLine(); var info = CurrentLogWindow.GetCurrentFileInfo(); - if (line != null && info != null) + if (line == null || info == null) { - ArgParser parser = new(toolEntry.Args); - var argLine = parser.BuildArgs(line, CurrentLogWindow.GetRealLineNum() + 1, info, this); - if (argLine != null) - { - StartTool(toolEntry.Cmd, argLine, toolEntry.Sysout, toolEntry.ColumnizerName, toolEntry.WorkingDir, true); - } + return; } + + ArgParser parser = new(toolEntry.Args); + var argLine = parser.BuildArgs(line, CurrentLogWindow.GetRealLineNum() + 1, info, this); + if (argLine == null) + { + return; + } + + request = new ToolLaunchRequest + { + Cmd = toolEntry.Cmd, + Args = argLine, + SysoutPipe = toolEntry.Sysout, + ColumnizerName = toolEntry.ColumnizerName, + WorkingDir = toolEntry.WorkingDir + }; } else { - StartTool(toolEntry.Cmd, string.Empty, toolEntry.Sysout, toolEntry.ColumnizerName, toolEntry.WorkingDir); - } - } - - [SupportedOSPlatform("windows")] - private void StartTool (string cmd, string args, bool sysoutPipe, string columnizerName, string workingDir, bool startWithOpenLog = false) - { - if (string.IsNullOrEmpty(cmd)) - { - return; - } + if (toolEntry.Sysout) + { + _ = MessageBox.Show(Resources.LogTabWindow_UI_Message_NoLogfileWithSysOutPipeToolConfigured, Resources.LogExpert_Common_UI_Title_LogExpert); + } - Process process = new(); - ProcessStartInfo startInfo = new(cmd, args); - if (!string.IsNullOrEmpty(workingDir)) - { - startInfo.WorkingDirectory = workingDir; + request = new ToolLaunchRequest + { + Cmd = toolEntry.Cmd, + Args = string.Empty, + SysoutPipe = false, + ColumnizerName = toolEntry.ColumnizerName, + WorkingDir = toolEntry.WorkingDir + }; } - process.StartInfo = startInfo; - process.EnableRaisingEvents = true; + var result = _toolLaunchService.Launch(request); - if (sysoutPipe && !startWithOpenLog) + if (result.HasError) { - _ = MessageBox.Show(Resources.LogTabWindow_UI_Message_NoLogfileWithSysOutPipeToolConfigured, Resources.LogExpert_Common_UI_Title_LogExpert); + _ = MessageBox.Show(result.ErrorMessage, Resources.LogExpert_Common_UI_Title_LogExpert); + return; } - if (sysoutPipe && startWithOpenLog) + if (result.PipeFileName != null) { - var columnizer = ColumnizerPicker.DecideMemoryColumnizerByName(columnizerName, PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers); + var title = CurrentLogWindow!.IsTempFile + ? CurrentLogWindow.TempTitleName + : $"{Util.GetNameFromPath(CurrentLogWindow.FileName)}{Resources.LogTabWindow_UI_LogWindow_Title_ExternalStartTool_Suffix}"; - //_logger.Info($"Starting external tool with sysout redirection: {cmd} {args}")); - startInfo.UseShellExecute = false; - startInfo.RedirectStandardOutput = true; - //process.OutputDataReceived += pipe.DataReceivedEventHandler; - try + var logWin = AddTempFileTab(result.PipeFileName, title); + if (result.Columnizer != null) { - _ = process.Start(); + logWin.ForceColumnizer(result.Columnizer); } - catch (Exception e) when (e is Win32Exception or - InvalidOperationException or - ObjectDisposedException or - PlatformNotSupportedException) - { - _logger.Error(e); - _ = MessageBox.Show(e.Message, Resources.LogExpert_Common_UI_Title_LogExpert); - return; - } - - SysoutPipe pipe = new(process.StandardOutput); - - var logWin = AddTempFileTab(pipe.FileName, - CurrentLogWindow.IsTempFile - ? CurrentLogWindow.TempTitleName - : $"{Util.GetNameFromPath(CurrentLogWindow.FileName)}{Resources.LogTabWindow_UI_LogWindow_Title_ExternalStartTool_Suffix}"); - logWin.ForceColumnizer(columnizer); - - process.Exited += pipe.ProcessExitedEventHandler; - //process.BeginOutputReadLine(); - } - else - { - StartExternalTool(process, startInfo); - } - } - - private static void StartExternalTool (Process process, ProcessStartInfo startInfo) - { - try - { - startInfo.UseShellExecute = false; - _ = process.Start(); - } - catch (Exception e) when (e is Win32Exception or - InvalidOperationException or - ObjectDisposedException or - PlatformNotSupportedException) - { - _logger.Error(e); - _ = MessageBox.Show(e.Message, Resources.LogExpert_Common_UI_Title_LogExpert); } } diff --git a/src/LogExpert.UI/Interface/IToolLaunchService.cs b/src/LogExpert.UI/Interface/IToolLaunchService.cs new file mode 100644 index 00000000..29aad853 --- /dev/null +++ b/src/LogExpert.UI/Interface/IToolLaunchService.cs @@ -0,0 +1,8 @@ +using LogExpert.UI.Services.ToolLaunchService; + +namespace LogExpert.UI.Interface; + +internal interface IToolLaunchService +{ + ToolLaunchResult Launch (ToolLaunchRequest request); +} diff --git a/src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchRequest.cs b/src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchRequest.cs new file mode 100644 index 00000000..ef34d541 --- /dev/null +++ b/src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchRequest.cs @@ -0,0 +1,14 @@ +namespace LogExpert.UI.Services.ToolLaunchService; + +internal sealed record ToolLaunchRequest +{ + public required string Cmd { get; init; } + + public string Args { get; init; } = string.Empty; + + public bool SysoutPipe { get; init; } + + public string? ColumnizerName { get; init; } + + public string? WorkingDir { get; init; } +} diff --git a/src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchResult.cs b/src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchResult.cs new file mode 100644 index 00000000..5ba59456 --- /dev/null +++ b/src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchResult.cs @@ -0,0 +1,14 @@ +using ColumnizerLib; + +namespace LogExpert.UI.Services.ToolLaunchService; + +internal readonly record struct ToolLaunchResult +{ + public bool HasError { get; init; } + + public string? ErrorMessage { get; init; } + + public string? PipeFileName { get; init; } + + public ILogLineMemoryColumnizer? Columnizer { get; init; } +} diff --git a/src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchService.cs b/src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchService.cs new file mode 100644 index 00000000..0fde51ab --- /dev/null +++ b/src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchService.cs @@ -0,0 +1,105 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.Versioning; + +using LogExpert.Core.Classes; +using LogExpert.Core.Classes.Columnizer; +using LogExpert.Core.Interfaces; +using LogExpert.UI.Interface; + +using NLog; + +namespace LogExpert.UI.Services.ToolLaunchService; + +[SupportedOSPlatform("windows")] +internal sealed class ToolLaunchService (IPluginRegistry pluginRegistry) : IToolLaunchService +{ + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + public ToolLaunchResult Launch (ToolLaunchRequest request) + { + return string.IsNullOrEmpty(request.Cmd) + ? new ToolLaunchResult + { + HasError = true, + ErrorMessage = "Command must not be empty." + } + : request.SysoutPipe + ? LaunchWithSysoutPipe(request) + : LaunchExternal(request); + } + + private static ToolLaunchResult LaunchExternal (ToolLaunchRequest request) + { + var startInfo = BuildStartInfo(request); + + startInfo.UseShellExecute = false; + + (bool flowControl, ToolLaunchResult value, _) = LaunchProcess(startInfo); + + return !flowControl + ? value + : new ToolLaunchResult { HasError = false }; + } + + private static (bool flowControl, ToolLaunchResult value, Process process) LaunchProcess (ProcessStartInfo startInfo) + { + using Process process = new() { StartInfo = startInfo, EnableRaisingEvents = true }; + + try + { + _ = process.Start(); + } + catch (Exception e) when (e is Win32Exception or + InvalidOperationException or + ObjectDisposedException or + PlatformNotSupportedException) + { + _logger.Error(e); + return (false, new ToolLaunchResult { HasError = true, ErrorMessage = e.Message }, process); + } + + return (true, default, process); + } + + private ToolLaunchResult LaunchWithSysoutPipe (ToolLaunchRequest request) + { + var columnizer = string.IsNullOrEmpty(request.ColumnizerName) + ? null + : ColumnizerPicker.DecideMemoryColumnizerByName(request.ColumnizerName, pluginRegistry.RegisteredColumnizers); + + var startInfo = BuildStartInfo(request); + startInfo.UseShellExecute = false; + startInfo.RedirectStandardOutput = true; + + (bool flowControl, ToolLaunchResult value, Process process) = LaunchProcess(startInfo); + + if (!flowControl) + { + return value; + } + + // TODO: SysoutPipe temp file is never deleted — fire-and-forget lifetime by design. + SysoutPipe pipe = new(process.StandardOutput); + process.Exited += pipe.ProcessExitedEventHandler; + + return new ToolLaunchResult + { + HasError = false, + PipeFileName = pipe.FileName, + Columnizer = columnizer + }; + } + + private static ProcessStartInfo BuildStartInfo (ToolLaunchRequest request) + { + var startInfo = new ProcessStartInfo(request.Cmd, request.Args); + + if (!string.IsNullOrEmpty(request.WorkingDir)) + { + startInfo.WorkingDirectory = request.WorkingDir; + } + + return startInfo; + } +} diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index be4694d6..c5abc25e 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-09 06:55:04 UTC + /// Generated: 2026-06-09 07:11:53 UTC /// Configuration: Release /// Plugin count: 21 /// @@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "3D6748C5E935505FFBFE24830208B11FAACF4EDCD672FBEABA4DD239B05D927A", + ["AutoColumnizer.dll"] = "F06B072F21BED58FC98DC9FD89C081BB0786DD7AB9326562C4ED2D687598EF6B", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "D5435B1886C4FA665A59FB647CE488C092CD2BF0587BB219B3A623C656BA8BF6", - ["CsvColumnizer.dll (x86)"] = "D5435B1886C4FA665A59FB647CE488C092CD2BF0587BB219B3A623C656BA8BF6", - ["DefaultPlugins.dll"] = "675EACAA0B1700FC6163ED29058124553DF9D6E935020CF04B38DAA9F33C279C", - ["FlashIconHighlighter.dll"] = "47605D179B3646F81BF44E8626251CDD30752C0535293AF4C4AA89A1CAF76B06", - ["GlassfishColumnizer.dll"] = "1C30B9D350C46B33A76C96C88DA4B9DCF2B1474BB1ACD92461878365F704A2EC", - ["JsonColumnizer.dll"] = "27F5ED050CEDBC1BD72250DD7C9FC330DB32825FE04FB9C899376BF45F347D92", - ["JsonCompactColumnizer.dll"] = "08F189DE85381709A10DD506DA2DC1F4D9885C68AD0A63799557166295CAD590", - ["Log4jXmlColumnizer.dll"] = "D21498D4B727AA8F2DAF59CD470AEEB11603FF64936E8AB11EDD92A1370BC681", - ["LogExpert.Resources.dll"] = "C8390BB994FACB178D34EC2023F8862F685489549F52726FD9E742B6497B7781", + ["CsvColumnizer.dll"] = "E5020059F5ADC71066E519EB94DF5C2979D3135FAC6E2923B50C3FBB62822987", + ["CsvColumnizer.dll (x86)"] = "E5020059F5ADC71066E519EB94DF5C2979D3135FAC6E2923B50C3FBB62822987", + ["DefaultPlugins.dll"] = "DA4B2B5CFBF0B0FF928C3A1155F94E26622509DADEBDB03D25C25CC8238CC1D0", + ["FlashIconHighlighter.dll"] = "DE097E728DB95A93703DBA54E3D69AADC4B9F1567C89CD2AAF5B942BD4D363E4", + ["GlassfishColumnizer.dll"] = "4A6A43B08AF6808AEC6727B3EAA5D287775FDA16D0443BB113A8ABA2AD426F0A", + ["JsonColumnizer.dll"] = "36325E7C57B08F57DD42EC666AE9072CBC0B0BD407680063CFCBF3696A1D1429", + ["JsonCompactColumnizer.dll"] = "B2301944FB40C4ECC1CD9591AEF49666D394A0D1EC0C08B713D057B0E2569297", + ["Log4jXmlColumnizer.dll"] = "B10511AED306EAC7B875E8AE91CDC2A906FC1B1EF407CD908AAC8530DF52F4D6", + ["LogExpert.Resources.dll"] = "F4ACED3693B4A21BAEE1840FCD039134D78B758C2935561D4B42AF2D1354D6EE", ["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"] = "76A856BE58511F40B5E89D7F100AA7843AC62CD380717F924CCFDC234375BDAD", - ["SftpFileSystem.dll"] = "73D71AF4831468722C327DE246879343B9F95E4BA931B07F03A4B60C70B4A2A2", - ["SftpFileSystem.dll (x86)"] = "107BD9DF13EB1C0FB7F8B4C1B0D77B79A62A804FEA5F656FFADEBCDE3FFD4E5B", - ["SftpFileSystem.Resources.dll"] = "12539CBB43138C4D1AF4FFFC0D2D6370B6969E358DF9A03FD29720CA18D34287", - ["SftpFileSystem.Resources.dll (x86)"] = "12539CBB43138C4D1AF4FFFC0D2D6370B6969E358DF9A03FD29720CA18D34287", + ["RegexColumnizer.dll"] = "D8665A762EECCB8516C8D51A0D7099D53C8C515A66DB6D88AA86F60484E6CBB1", + ["SftpFileSystem.dll"] = "D35385D7B4A20946A1CF987C277682FD096FD93A3BB90A4E7944D41556DEED77", + ["SftpFileSystem.dll (x86)"] = "0C9AA4F229A76476257288E683D41830A09AC0E3945A717823193B579A7B3EEB", + ["SftpFileSystem.Resources.dll"] = "EF5A68C3448B3BEDD796BE2BDA85BE7E9242D6A1F63C207D95305BE325E48FCF", + ["SftpFileSystem.Resources.dll (x86)"] = "EF5A68C3448B3BEDD796BE2BDA85BE7E9242D6A1F63C207D95305BE325E48FCF", }; }