Skip to content

Commit bd1d695

Browse files
authored
Merge pull request #600 from LogExperts/toolservice
Start Tool into its own service
2 parents f7f7ec7 + 942a3a1 commit bd1d695

7 files changed

Lines changed: 265 additions & 92 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System.Runtime.Versioning;
2+
3+
using LogExpert.Core.Interfaces;
4+
using LogExpert.UI.Services.ToolLaunchService;
5+
6+
using Moq;
7+
8+
using NUnit.Framework;
9+
10+
namespace LogExpert.Tests.Services;
11+
12+
[TestFixture]
13+
[Apartment(ApartmentState.STA)]
14+
[SupportedOSPlatform("windows")]
15+
internal class ToolLaunchServiceTests
16+
{
17+
private Mock<IPluginRegistry> _pluginRegistryMock = null!;
18+
private ToolLaunchService _sut = null!;
19+
20+
[SetUp]
21+
public void SetUp ()
22+
{
23+
_pluginRegistryMock = new Mock<IPluginRegistry>();
24+
_ = _pluginRegistryMock.Setup(pr => pr.RegisteredColumnizers).Returns([]);
25+
26+
_sut = new ToolLaunchService(_pluginRegistryMock.Object);
27+
}
28+
29+
[Test]
30+
public void Launch_WithEmptyCmd_ReturnsHasErrorTrue ()
31+
{
32+
var request = new ToolLaunchRequest { Cmd = string.Empty, Args = string.Empty, SysoutPipe = false };
33+
34+
var result = _sut.Launch(request);
35+
36+
Assert.That(result.HasError, Is.True);
37+
Assert.That(result.ErrorMessage, Is.Not.Null.And.Not.Empty);
38+
}
39+
40+
[Test]
41+
public void Launch_WithValidCmdAndNoSysoutPipe_ReturnsSuccessWithNullPipeFileName ()
42+
{
43+
var request = new ToolLaunchRequest { Cmd = "cmd.exe", Args = "/c exit 0", SysoutPipe = false };
44+
45+
var result = _sut.Launch(request);
46+
47+
Assert.That(result.HasError, Is.False);
48+
Assert.That(result.PipeFileName, Is.Null);
49+
}
50+
51+
[Test]
52+
public void Launch_WithValidCmdAndSysoutPipe_ReturnsPipeFileNamePointingToExistingFile ()
53+
{
54+
var request = new ToolLaunchRequest { Cmd = "cmd.exe", Args = "/c echo hello", SysoutPipe = true };
55+
56+
var result = _sut.Launch(request);
57+
58+
Assert.That(result.HasError, Is.False);
59+
Assert.That(result.PipeFileName, Is.Not.Null.And.Not.Empty);
60+
Assert.That(File.Exists(result.PipeFileName), Is.True);
61+
}
62+
}

src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs

Lines changed: 46 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
using LogExpert.UI.Services.MenuToolbarService;
2828
using LogExpert.UI.Services.SessionHandlerService;
2929
using LogExpert.UI.Services.TabControllerService;
30+
using LogExpert.UI.Services.ToolLaunchService;
3031
using LogExpert.UI.Services.ToolWindowCoordinatorService;
3132

3233
using NLog;
@@ -54,6 +55,7 @@ internal partial class LogTabWindow : Form, ILogTabWindow
5455
private readonly ToolWindowCoordinator _toolWindowCoordinator;
5556
private readonly FileOperationService _fileOperationService;
5657
private readonly SessionHandler _sessionHandler;
58+
private readonly ToolLaunchService _toolLaunchService;
5759

5860
private bool _disposed;
5961

@@ -114,6 +116,7 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu
114116
_fileOperationService.FileOpened += OnFileOperationServiceFileOpened;
115117

116118
_sessionHandler = new SessionHandler(PluginRegistry.PluginRegistry.Instance, request => _fileOperationService.AddFileTab(request));
119+
_toolLaunchService = new ToolLaunchService(PluginRegistry.PluginRegistry.Instance);
117120

118121
_logWindowCoordinator = new LogWindowCoordinator(configManager, PluginRegistry.PluginRegistry.Instance, this, _tabController, _ledService, _fileOperationService);
119122

@@ -1328,102 +1331,69 @@ private void ToolButtonClick (ToolEntry toolEntry)
13281331
return;
13291332
}
13301333

1334+
ToolLaunchRequest request;
1335+
13311336
if (CurrentLogWindow != null)
13321337
{
13331338
var line = CurrentLogWindow.GetCurrentLine();
13341339
var info = CurrentLogWindow.GetCurrentFileInfo();
1335-
if (line != null && info != null)
1340+
if (line == null || info == null)
13361341
{
1337-
ArgParser parser = new(toolEntry.Args);
1338-
var argLine = parser.BuildArgs(line, CurrentLogWindow.GetRealLineNum() + 1, info, this);
1339-
if (argLine != null)
1340-
{
1341-
StartTool(toolEntry.Cmd, argLine, toolEntry.Sysout, toolEntry.ColumnizerName, toolEntry.WorkingDir, true);
1342-
}
1342+
return;
13431343
}
1344+
1345+
ArgParser parser = new(toolEntry.Args);
1346+
var argLine = parser.BuildArgs(line, CurrentLogWindow.GetRealLineNum() + 1, info, this);
1347+
if (argLine == null)
1348+
{
1349+
return;
1350+
}
1351+
1352+
request = new ToolLaunchRequest
1353+
{
1354+
Cmd = toolEntry.Cmd,
1355+
Args = argLine,
1356+
SysoutPipe = toolEntry.Sysout,
1357+
ColumnizerName = toolEntry.ColumnizerName,
1358+
WorkingDir = toolEntry.WorkingDir
1359+
};
13441360
}
13451361
else
13461362
{
1347-
StartTool(toolEntry.Cmd, string.Empty, toolEntry.Sysout, toolEntry.ColumnizerName, toolEntry.WorkingDir);
1348-
}
1349-
}
1350-
1351-
[SupportedOSPlatform("windows")]
1352-
private void StartTool (string cmd, string args, bool sysoutPipe, string columnizerName, string workingDir, bool startWithOpenLog = false)
1353-
{
1354-
if (string.IsNullOrEmpty(cmd))
1355-
{
1356-
return;
1357-
}
1363+
if (toolEntry.Sysout)
1364+
{
1365+
_ = MessageBox.Show(Resources.LogTabWindow_UI_Message_NoLogfileWithSysOutPipeToolConfigured, Resources.LogExpert_Common_UI_Title_LogExpert);
1366+
}
13581367

1359-
Process process = new();
1360-
ProcessStartInfo startInfo = new(cmd, args);
1361-
if (!string.IsNullOrEmpty(workingDir))
1362-
{
1363-
startInfo.WorkingDirectory = workingDir;
1368+
request = new ToolLaunchRequest
1369+
{
1370+
Cmd = toolEntry.Cmd,
1371+
Args = string.Empty,
1372+
SysoutPipe = false,
1373+
ColumnizerName = toolEntry.ColumnizerName,
1374+
WorkingDir = toolEntry.WorkingDir
1375+
};
13641376
}
13651377

1366-
process.StartInfo = startInfo;
1367-
process.EnableRaisingEvents = true;
1378+
var result = _toolLaunchService.Launch(request);
13681379

1369-
if (sysoutPipe && !startWithOpenLog)
1380+
if (result.HasError)
13701381
{
1371-
_ = MessageBox.Show(Resources.LogTabWindow_UI_Message_NoLogfileWithSysOutPipeToolConfigured, Resources.LogExpert_Common_UI_Title_LogExpert);
1382+
_ = MessageBox.Show(result.ErrorMessage, Resources.LogExpert_Common_UI_Title_LogExpert);
1383+
return;
13721384
}
13731385

1374-
if (sysoutPipe && startWithOpenLog)
1386+
if (result.PipeFileName != null)
13751387
{
1376-
var columnizer = ColumnizerPicker.DecideMemoryColumnizerByName(columnizerName, PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers);
1388+
var title = CurrentLogWindow!.IsTempFile
1389+
? CurrentLogWindow.TempTitleName
1390+
: $"{Util.GetNameFromPath(CurrentLogWindow.FileName)}{Resources.LogTabWindow_UI_LogWindow_Title_ExternalStartTool_Suffix}";
13771391

1378-
//_logger.Info($"Starting external tool with sysout redirection: {cmd} {args}"));
1379-
startInfo.UseShellExecute = false;
1380-
startInfo.RedirectStandardOutput = true;
1381-
//process.OutputDataReceived += pipe.DataReceivedEventHandler;
1382-
try
1392+
var logWin = AddTempFileTab(result.PipeFileName, title);
1393+
if (result.Columnizer != null)
13831394
{
1384-
_ = process.Start();
1395+
logWin.ForceColumnizer(result.Columnizer);
13851396
}
1386-
catch (Exception e) when (e is Win32Exception or
1387-
InvalidOperationException or
1388-
ObjectDisposedException or
1389-
PlatformNotSupportedException)
1390-
{
1391-
_logger.Error(e);
1392-
_ = MessageBox.Show(e.Message, Resources.LogExpert_Common_UI_Title_LogExpert);
1393-
return;
1394-
}
1395-
1396-
SysoutPipe pipe = new(process.StandardOutput);
1397-
1398-
var logWin = AddTempFileTab(pipe.FileName,
1399-
CurrentLogWindow.IsTempFile
1400-
? CurrentLogWindow.TempTitleName
1401-
: $"{Util.GetNameFromPath(CurrentLogWindow.FileName)}{Resources.LogTabWindow_UI_LogWindow_Title_ExternalStartTool_Suffix}");
1402-
logWin.ForceColumnizer(columnizer);
1403-
1404-
process.Exited += pipe.ProcessExitedEventHandler;
1405-
//process.BeginOutputReadLine();
1406-
}
1407-
else
1408-
{
1409-
StartExternalTool(process, startInfo);
1410-
}
1411-
}
1412-
1413-
private static void StartExternalTool (Process process, ProcessStartInfo startInfo)
1414-
{
1415-
try
1416-
{
1417-
startInfo.UseShellExecute = false;
1418-
_ = process.Start();
1419-
}
1420-
catch (Exception e) when (e is Win32Exception or
1421-
InvalidOperationException or
1422-
ObjectDisposedException or
1423-
PlatformNotSupportedException)
1424-
{
1425-
_logger.Error(e);
1426-
_ = MessageBox.Show(e.Message, Resources.LogExpert_Common_UI_Title_LogExpert);
14271397
}
14281398
}
14291399

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using LogExpert.UI.Services.ToolLaunchService;
2+
3+
namespace LogExpert.UI.Interface;
4+
5+
internal interface IToolLaunchService
6+
{
7+
ToolLaunchResult Launch (ToolLaunchRequest request);
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace LogExpert.UI.Services.ToolLaunchService;
2+
3+
internal sealed record ToolLaunchRequest
4+
{
5+
public required string Cmd { get; init; }
6+
7+
public string Args { get; init; } = string.Empty;
8+
9+
public bool SysoutPipe { get; init; }
10+
11+
public string? ColumnizerName { get; init; }
12+
13+
public string? WorkingDir { get; init; }
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using ColumnizerLib;
2+
3+
namespace LogExpert.UI.Services.ToolLaunchService;
4+
5+
internal readonly record struct ToolLaunchResult
6+
{
7+
public bool HasError { get; init; }
8+
9+
public string? ErrorMessage { get; init; }
10+
11+
public string? PipeFileName { get; init; }
12+
13+
public ILogLineMemoryColumnizer? Columnizer { get; init; }
14+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics;
3+
using System.Runtime.Versioning;
4+
5+
using LogExpert.Core.Classes;
6+
using LogExpert.Core.Classes.Columnizer;
7+
using LogExpert.Core.Interfaces;
8+
using LogExpert.UI.Interface;
9+
10+
using NLog;
11+
12+
namespace LogExpert.UI.Services.ToolLaunchService;
13+
14+
[SupportedOSPlatform("windows")]
15+
internal sealed class ToolLaunchService (IPluginRegistry pluginRegistry) : IToolLaunchService
16+
{
17+
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
18+
19+
public ToolLaunchResult Launch (ToolLaunchRequest request)
20+
{
21+
return string.IsNullOrEmpty(request.Cmd)
22+
? new ToolLaunchResult
23+
{
24+
HasError = true,
25+
ErrorMessage = "Command must not be empty."
26+
}
27+
: request.SysoutPipe
28+
? LaunchWithSysoutPipe(request)
29+
: LaunchExternal(request);
30+
}
31+
32+
private static ToolLaunchResult LaunchExternal (ToolLaunchRequest request)
33+
{
34+
var startInfo = BuildStartInfo(request);
35+
36+
startInfo.UseShellExecute = false;
37+
38+
(bool flowControl, ToolLaunchResult value, _) = LaunchProcess(startInfo);
39+
40+
return !flowControl
41+
? value
42+
: new ToolLaunchResult { HasError = false };
43+
}
44+
45+
private static (bool flowControl, ToolLaunchResult value, Process process) LaunchProcess (ProcessStartInfo startInfo)
46+
{
47+
using Process process = new() { StartInfo = startInfo, EnableRaisingEvents = true };
48+
49+
try
50+
{
51+
_ = process.Start();
52+
}
53+
catch (Exception e) when (e is Win32Exception or
54+
InvalidOperationException or
55+
ObjectDisposedException or
56+
PlatformNotSupportedException)
57+
{
58+
_logger.Error(e);
59+
return (false, new ToolLaunchResult { HasError = true, ErrorMessage = e.Message }, process);
60+
}
61+
62+
return (true, default, process);
63+
}
64+
65+
private ToolLaunchResult LaunchWithSysoutPipe (ToolLaunchRequest request)
66+
{
67+
var columnizer = string.IsNullOrEmpty(request.ColumnizerName)
68+
? null
69+
: ColumnizerPicker.DecideMemoryColumnizerByName(request.ColumnizerName, pluginRegistry.RegisteredColumnizers);
70+
71+
var startInfo = BuildStartInfo(request);
72+
startInfo.UseShellExecute = false;
73+
startInfo.RedirectStandardOutput = true;
74+
75+
(bool flowControl, ToolLaunchResult value, Process process) = LaunchProcess(startInfo);
76+
77+
if (!flowControl)
78+
{
79+
return value;
80+
}
81+
82+
// TODO: SysoutPipe temp file is never deleted — fire-and-forget lifetime by design.
83+
SysoutPipe pipe = new(process.StandardOutput);
84+
process.Exited += pipe.ProcessExitedEventHandler;
85+
86+
return new ToolLaunchResult
87+
{
88+
HasError = false,
89+
PipeFileName = pipe.FileName,
90+
Columnizer = columnizer
91+
};
92+
}
93+
94+
private static ProcessStartInfo BuildStartInfo (ToolLaunchRequest request)
95+
{
96+
var startInfo = new ProcessStartInfo(request.Cmd, request.Args);
97+
98+
if (!string.IsNullOrEmpty(request.WorkingDir))
99+
{
100+
startInfo.WorkingDirectory = request.WorkingDir;
101+
}
102+
103+
return startInfo;
104+
}
105+
}

0 commit comments

Comments
 (0)