Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/LogExpert.Tests/Services/ToolLaunchServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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<IPluginRegistry> _pluginRegistryMock = null!;
private ToolLaunchService _sut = null!;

[SetUp]
public void SetUp ()
{
_pluginRegistryMock = new Mock<IPluginRegistry>();
_ = _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);
}
}
122 changes: 46 additions & 76 deletions src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/LogExpert.UI/Interface/IToolLaunchService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using LogExpert.UI.Services.ToolLaunchService;

namespace LogExpert.UI.Interface;

internal interface IToolLaunchService
{
ToolLaunchResult Launch (ToolLaunchRequest request);
}
14 changes: 14 additions & 0 deletions src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchRequest.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
14 changes: 14 additions & 0 deletions src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchResult.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
105 changes: 105 additions & 0 deletions src/LogExpert.UI/Services/ToolLaunchService/ToolLaunchService.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading