From c0385c07822be0644d185f0d90df193ccbf5dfa5 Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Mon, 27 Oct 2025 11:38:43 +0100 Subject: [PATCH] bugfix Parallel CLI Runner Add parallel execution tests and improve cancellation handling in `CliRunner` - Introduced new tests (`RunMultipleTimesTestProgram_CheckParalelWork` and `RunMultipleTimesTestProgram_CheckParalelWorkCumulative`) to verify parallel execution behavior. - Enhanced `SendAsync` and `RunAndCaptureJsonAsync` methods in `CliRunner` with `CancellationToken` support for better task cancellation. - Refactored process output handling for improved clarity and robustness. - Added error handling during CLI logger initialization. --- NfcC7_DLL/NfcHanler/Utils/CliRunner.cs | 86 ++++++------ .../PoseidonCmdStartStop/CliRunner.cs | 88 +++++++------ .../PoseidonCmdStartStop/CliRunnerTest.cs | 122 ++++++++++++++++++ 3 files changed, 212 insertions(+), 84 deletions(-) diff --git a/NfcC7_DLL/NfcHanler/Utils/CliRunner.cs b/NfcC7_DLL/NfcHanler/Utils/CliRunner.cs index a76f64f15..4d7f092fa 100644 --- a/NfcC7_DLL/NfcHanler/Utils/CliRunner.cs +++ b/NfcC7_DLL/NfcHanler/Utils/CliRunner.cs @@ -64,15 +64,23 @@ namespace NfcC7_DLL.NfcHanler.Utils if (isCliLogging) { - /* log = new ScopedLoggerFactory().CreateLogger( - @"C:\TBF\Logs\CliRunner.txt", - 10, // maxFileSizeMB - 7, // maxBackups - log4net.Core.Level.Debug, - true, // zipRolledFiles - true, // singleZipPerDay - TimeSpan.FromMinutes(2) // zipScanInterval - );*/ + try + { + /*log = new ScopedLoggerFactory().CreateLogger( + @"C:\TBF\Logs\CliRunner.txt", + 10, // maxFileSizeMB + 7, // maxBackups + log4net.Core.Level.Debug, + true, // zipRolledFiles + true, // singleZipPerDay + TimeSpan.FromMinutes(2) // zipScanInterval + );*/ + } + catch (Exception ex) + { + // Fallback to console or handle gracefully + Console.WriteLine($"Failed to initialize logger: {ex.Message}"); + } } } @@ -103,7 +111,7 @@ namespace NfcC7_DLL.NfcHanler.Utils } - public async Task SendAsync(string fileName, string args) + public async Task SendAsync(string fileName, string args, CancellationToken ct = default) { var psi = new ProcessStartInfo { @@ -115,24 +123,28 @@ namespace NfcC7_DLL.NfcHanler.Utils CreateNoWindow = true }; - var process = new Process { StartInfo = psi }; - var sb = new StringBuilder(); - - process.OutputDataReceived += (sender, e) => - { - if (e.Data != null) - sb.AppendLine(e.Data); - }; - + using var process = new Process { StartInfo = psi, EnableRaisingEvents = true }; process.Start(); - process.BeginOutputReadLine(); - process.WaitForExit(); + Task stdOutTask = process.StandardOutput.ReadToEndAsync(); + Task stdErrTask = process.StandardError.ReadToEndAsync(); + + try + { + await Task.WhenAll(stdOutTask, stdErrTask, process.WaitForExitAsync(ct)); + } + catch (OperationCanceledException) + { + if (!process.HasExited) + { + process.Kill(); + } - // Now all outputs are available - string allOutput = sb.ToString(); + throw; + } + + string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? ""); //log?.Debug(allOutput); - return allOutput; } @@ -143,7 +155,7 @@ namespace NfcC7_DLL.NfcHanler.Utils return task; } - public async Task RunAndCaptureJsonAsync(string fileName, string args) where T : new() + public async Task RunAndCaptureJsonAsync(string fileName, string args, CancellationToken ct = default) where T : new() { var psi = new ProcessStartInfo { @@ -155,29 +167,19 @@ namespace NfcC7_DLL.NfcHanler.Utils CreateNoWindow = true }; - var process = new Process { StartInfo = psi }; - var sb = new StringBuilder(); - - process.OutputDataReceived += (sender, e) => - { - if (e.Data != null) - sb.AppendLine(e.Data); - }; - + using var process = new Process { StartInfo = psi, EnableRaisingEvents = true }; process.Start(); - process.BeginOutputReadLine(); - process.WaitForExit(); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); - // Now sb contains all output text - string allOutput = sb.ToString(); + await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct)); + + string allOutput = (stdoutTask.Result ?? "") + (stderrTask.Result ?? ""); //log?.Debug(allOutput); - // Extract JSON part string json = ExtractJson(allOutput); - - if (TryJsonStringDeserialize(json, out T runAndCaptureJsonAsync)) return runAndCaptureJsonAsync; - + if (TryJsonStringDeserialize(json, out T result)) return result; return default; } diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs index 45567a7d3..3dabe2d01 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using log4net; using log4net.Config; @@ -53,15 +54,23 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop if (isCliLogging) { - log = new ScopedLoggerFactory().CreateLogger( - @"C:\TBF\Logs\CliRunner.txt", - 10, // maxFileSizeMB - 7, // maxBackups - log4net.Core.Level.Debug, - true, // zipRolledFiles - true, // singleZipPerDay - TimeSpan.FromMinutes(2) // zipScanInterval - ); + try + { + log = new ScopedLoggerFactory().CreateLogger( + @"C:\TBF\Logs\CliRunner.txt", + 10, // maxFileSizeMB + 7, // maxBackups + log4net.Core.Level.Debug, + true, // zipRolledFiles + true, // singleZipPerDay + TimeSpan.FromMinutes(2) // zipScanInterval + ); + } + catch (Exception ex) + { + // Fallback to console or handle gracefully + Console.WriteLine($"Failed to initialize logger: {ex.Message}"); + } } } @@ -84,7 +93,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop } - public async Task SendAsync(string fileName, string args) + public async Task SendAsync(string fileName, string args, CancellationToken ct = default) { var psi = new ProcessStartInfo { @@ -96,26 +105,31 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop CreateNoWindow = true }; - var process = new Process { StartInfo = psi }; - var sb = new StringBuilder(); - - process.OutputDataReceived += (sender, e) => - { - if (e.Data != null) - sb.AppendLine(e.Data); - }; - + using var process = new Process { StartInfo = psi, EnableRaisingEvents = true }; process.Start(); - process.BeginOutputReadLine(); - process.WaitForExit(); + Task stdOutTask = process.StandardOutput.ReadToEndAsync(); + Task stdErrTask = process.StandardError.ReadToEndAsync(); + + try + { + await Task.WhenAll(stdOutTask, stdErrTask, process.WaitForExitAsync(ct)); + } + catch (OperationCanceledException) + { + if (!process.HasExited) + { + process.Kill(); + } - // Now all outputs are available - string allOutput = sb.ToString(); + throw; + } + + string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? ""); log?.Debug(allOutput); - return allOutput; } + public void AddRunAndCaptureJsonAsync(SerialPortData data, SerialPortData.EMeterArg eMeterArg) where T : new() { @@ -123,7 +137,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop taskPool.Add(task); } - public async Task RunAndCaptureJsonAsync(string fileName, string args) where T : new() + public async Task RunAndCaptureJsonAsync(string fileName, string args, CancellationToken ct = default) where T : new() { var psi = new ProcessStartInfo { @@ -135,29 +149,19 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop CreateNoWindow = true }; - var process = new Process { StartInfo = psi }; - var sb = new StringBuilder(); - - process.OutputDataReceived += (sender, e) => - { - if (e.Data != null) - sb.AppendLine(e.Data); - }; - + using var process = new Process { StartInfo = psi, EnableRaisingEvents = true }; process.Start(); - process.BeginOutputReadLine(); - process.WaitForExit(); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); - // Now sb contains all output text - string allOutput = sb.ToString(); + await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct)); + + string allOutput = (stdoutTask.Result ?? "") + (stderrTask.Result ?? ""); log?.Debug(allOutput); - // Extract JSON part string json = ExtractJson(allOutput); - - if (TryJsonStringDeserialize(json, out T runAndCaptureJsonAsync)) return runAndCaptureJsonAsync; - + if (TryJsonStringDeserialize(json, out T result)) return result; return default; } diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTest.cs index f0c3a6ff6..df1635ed5 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTest.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.VisualStudio.TestTools.UnitTesting; using TBF.Rig.RegisterReaders.PoseidonCmdStartStop; @@ -46,5 +49,124 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop Assert.AreEqual(true, dto.NfcTagDetected); Assert.AreEqual(74, dto.ProductType); } + + [TestMethod] + public void RunMultipleTimesTestProgram_CheckParalelWork() + { + SerialPortData serialPort = new SerialPortData("COM3","cmdSleepTest.exe",74); + + // Check if the executable exists in current directory + if (!System.IO.File.Exists(serialPort.SerialPortCmdClientPath)) + { + Assert.Inconclusive($"Test executable '{serialPort.SerialPortCmdClientPath}' not found. Please ensure cmdSleepTest.exe exists in the test directory."); + return; + } + CliRunner cliRunner = new CliRunner(false); + + CliRunner cliRunnerOne = new CliRunner(false); + DateTime StartTime = DateTime.Now; + cliRunnerOne.AddRunAndCaptureJsonAsync(serialPort, SerialPortData.EMeterArg.AllParams); + cliRunnerOne.WaitAll(); + TimeSpan OneEndTime = DateTime.Now - StartTime; + + StartTime = DateTime.Now; + Console.WriteLine($"Start Time Loop: {StartTime.ToString("yyyy-MM-dd HH:mm:ss")}"); + + for (int i = 0; i < 20; i++) + { + cliRunner.AddRunAndCaptureJsonAsync(serialPort, SerialPortData.EMeterArg.AllParams); + } + cliRunner.WaitAll(); + + DateTime EndTime = DateTime.Now; + Console.WriteLine($"End Time Loop: {EndTime.ToString("yyyy-MM-dd HH:mm:ss")}"); + TimeSpan delta = EndTime - StartTime; + Console.WriteLine($"Delta Time One process {OneEndTime.Hours:D2}:{OneEndTime.Minutes:D2}:{OneEndTime.Seconds:D2}.{OneEndTime.Milliseconds:D3} " + + $"vs Loop: {delta.Hours:D2}:{delta.Minutes:D2}:{delta.Seconds:D2}.{delta.Milliseconds:D3}"); + Console.WriteLine($"Total tasks: {cliRunner.TaskPool.Count}"); + + + foreach (Task task in cliRunner.TaskPool) + { + if (task != null) + { + JsonDataFromPoseidon jsonDataFromPoseidon = task.Result; + Console.WriteLine($"Result: {jsonDataFromPoseidon.DeviceId}"); + Assert.IsNotNull(jsonDataFromPoseidon); + } + } + + // Check that the total time is greater than the sum of the individual times + Assert.IsTrue((cliRunner.TaskPool.Count*OneEndTime.Ticks) > delta.Ticks); + + } + + [TestMethod] + public async Task RunMultipleTimesTestProgram_CheckParalelWorkCumulative() + { + SerialPortData serialPort = new SerialPortData("COM3","cmdSleepTest.exe",74); + + // Check if the executable exists in current directory + if (!System.IO.File.Exists(serialPort.SerialPortCmdClientPath)) + { + Assert.Inconclusive($"Test executable '{serialPort.SerialPortCmdClientPath}' not found. Please ensure cmdSleepTest.exe exists in the test directory."); + return; + } + + List cliRunnerList = new List(); + + for (int i = 0; i < 20; i++) + cliRunnerList.Add(new CliRunner(false)); + + CliRunner cliRunnerOne = new CliRunner(false); + DateTime StartTime = DateTime.Now; + cliRunnerOne.AddRunAndCaptureJsonAsync(serialPort, SerialPortData.EMeterArg.AllParams); + cliRunnerOne.WaitAll(); + TimeSpan OneEndTime = DateTime.Now - StartTime; + + StartTime = DateTime.Now; + Console.WriteLine($"Start Time Loop: {StartTime.ToString("yyyy-MM-dd HH:mm:ss")}"); + + for (int iCliRunner = 0; iCliRunner < cliRunnerList.Count; iCliRunner++) + { + cliRunnerList[iCliRunner].AddRunAndCaptureJsonAsync(serialPort, + SerialPortData.EMeterArg.AllParams); + + for (int i = 0; i < 20; i++) + { + cliRunnerList[iCliRunner].AddRunAndCaptureJsonAsync(serialPort, + SerialPortData.EMeterArg.AllParams); + } + } + + // Wait for ALL tasks from ALL runners + var allTasks = cliRunnerList.SelectMany(r => r.TaskPool).ToArray(); + await Task.WhenAll(allTasks); + + DateTime EndTime = DateTime.Now; + Console.WriteLine($"End Time Loop: {EndTime.ToString("yyyy-MM-dd HH:mm:ss")}"); + TimeSpan delta = EndTime - StartTime; + Console.WriteLine($"Delta Time One process {OneEndTime.Hours:D2}:{OneEndTime.Minutes:D2}:{OneEndTime.Seconds:D2}.{OneEndTime.Milliseconds:D3} " + + $"vs Loop: {delta.Hours:D2}:{delta.Minutes:D2}:{delta.Seconds:D2}.{delta.Milliseconds:D3}"); + Console.WriteLine($"Total tasks: {cliRunnerList.Sum(runner => runner.TaskPool.Count)}"); + + for (int iCliRunner = 0; iCliRunner < cliRunnerList.Count; iCliRunner++) + { + string results = $"iCliRunner: {iCliRunner}"; + foreach (Task task in cliRunnerList[iCliRunner].TaskPool) + { + if (task != null) + { + JsonDataFromPoseidon jsonDataFromPoseidon = task.Result; + results += ($" ID: {jsonDataFromPoseidon.DeviceId}"); + Assert.IsNotNull(jsonDataFromPoseidon); + } + } + Console.WriteLine(results); + } + + // Check that the total time is greater than the sum of the individual times + Assert.IsTrue((cliRunnerList.Count*OneEndTime.Ticks) > delta.Ticks); + } } } \ No newline at end of file