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.
This commit is contained in:
Michal Buzik 2025-10-27 11:38:43 +01:00
parent db43bdb652
commit c0385c0782
3 changed files with 212 additions and 84 deletions

View File

@ -64,15 +64,23 @@ namespace NfcC7_DLL.NfcHanler.Utils
if (isCliLogging) if (isCliLogging)
{ {
/* log = new ScopedLoggerFactory().CreateLogger<CliRunner>( try
@"C:\TBF\Logs\CliRunner.txt", {
10, // maxFileSizeMB /*log = new ScopedLoggerFactory().CreateLogger<CliRunner>(
7, // maxBackups @"C:\TBF\Logs\CliRunner.txt",
log4net.Core.Level.Debug, 10, // maxFileSizeMB
true, // zipRolledFiles 7, // maxBackups
true, // singleZipPerDay log4net.Core.Level.Debug,
TimeSpan.FromMinutes(2) // zipScanInterval 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<string> SendAsync(string fileName, string args) public async Task<string> SendAsync(string fileName, string args, CancellationToken ct = default)
{ {
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
@ -115,24 +123,28 @@ namespace NfcC7_DLL.NfcHanler.Utils
CreateNoWindow = true CreateNoWindow = true
}; };
var process = new Process { StartInfo = psi }; using var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
var sb = new StringBuilder();
process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
sb.AppendLine(e.Data);
};
process.Start(); process.Start();
process.BeginOutputReadLine();
process.WaitForExit(); Task<string> stdOutTask = process.StandardOutput.ReadToEndAsync();
Task<string> 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 throw;
string allOutput = sb.ToString(); }
string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? "");
//log?.Debug(allOutput); //log?.Debug(allOutput);
return allOutput; return allOutput;
} }
@ -143,7 +155,7 @@ namespace NfcC7_DLL.NfcHanler.Utils
return task; return task;
} }
public async Task<T> RunAndCaptureJsonAsync<T>(string fileName, string args) where T : new() public async Task<T> RunAndCaptureJsonAsync<T>(string fileName, string args, CancellationToken ct = default) where T : new()
{ {
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
@ -155,29 +167,19 @@ namespace NfcC7_DLL.NfcHanler.Utils
CreateNoWindow = true CreateNoWindow = true
}; };
var process = new Process { StartInfo = psi }; using var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
var sb = new StringBuilder();
process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
sb.AppendLine(e.Data);
};
process.Start(); process.Start();
process.BeginOutputReadLine();
process.WaitForExit(); var stdoutTask = process.StandardOutput.ReadToEndAsync();
var stderrTask = process.StandardError.ReadToEndAsync();
// Now sb contains all output text await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct));
string allOutput = sb.ToString();
string allOutput = (stdoutTask.Result ?? "") + (stderrTask.Result ?? "");
//log?.Debug(allOutput); //log?.Debug(allOutput);
// Extract JSON part
string json = ExtractJson(allOutput); string json = ExtractJson(allOutput);
if (TryJsonStringDeserialize(json, out T result)) return result;
if (TryJsonStringDeserialize(json, out T runAndCaptureJsonAsync)) return runAndCaptureJsonAsync;
return default; return default;
} }

View File

@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using log4net; using log4net;
using log4net.Config; using log4net.Config;
@ -53,15 +54,23 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
if (isCliLogging) if (isCliLogging)
{ {
log = new ScopedLoggerFactory().CreateLogger<CliRunner>( try
@"C:\TBF\Logs\CliRunner.txt", {
10, // maxFileSizeMB log = new ScopedLoggerFactory().CreateLogger<CliRunner>(
7, // maxBackups @"C:\TBF\Logs\CliRunner.txt",
log4net.Core.Level.Debug, 10, // maxFileSizeMB
true, // zipRolledFiles 7, // maxBackups
true, // singleZipPerDay log4net.Core.Level.Debug,
TimeSpan.FromMinutes(2) // zipScanInterval 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<string> SendAsync(string fileName, string args) public async Task<string> SendAsync(string fileName, string args, CancellationToken ct = default)
{ {
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
@ -96,26 +105,31 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
CreateNoWindow = true CreateNoWindow = true
}; };
var process = new Process { StartInfo = psi }; using var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
var sb = new StringBuilder();
process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
sb.AppendLine(e.Data);
};
process.Start(); process.Start();
process.BeginOutputReadLine();
process.WaitForExit(); Task<string> stdOutTask = process.StandardOutput.ReadToEndAsync();
Task<string> 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 throw;
string allOutput = sb.ToString(); }
string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? "");
log?.Debug(allOutput); log?.Debug(allOutput);
return allOutput; return allOutput;
} }
public void AddRunAndCaptureJsonAsync<T>(SerialPortData data, SerialPortData.EMeterArg eMeterArg) where T : new() public void AddRunAndCaptureJsonAsync<T>(SerialPortData data, SerialPortData.EMeterArg eMeterArg) where T : new()
{ {
@ -123,7 +137,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
taskPool.Add(task); taskPool.Add(task);
} }
public async Task<T> RunAndCaptureJsonAsync<T>(string fileName, string args) where T : new() public async Task<T> RunAndCaptureJsonAsync<T>(string fileName, string args, CancellationToken ct = default) where T : new()
{ {
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
@ -135,29 +149,19 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
CreateNoWindow = true CreateNoWindow = true
}; };
var process = new Process { StartInfo = psi }; using var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
var sb = new StringBuilder();
process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
sb.AppendLine(e.Data);
};
process.Start(); process.Start();
process.BeginOutputReadLine();
process.WaitForExit(); var stdoutTask = process.StandardOutput.ReadToEndAsync();
var stderrTask = process.StandardError.ReadToEndAsync();
// Now sb contains all output text await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct));
string allOutput = sb.ToString();
string allOutput = (stdoutTask.Result ?? "") + (stderrTask.Result ?? "");
log?.Debug(allOutput); log?.Debug(allOutput);
// Extract JSON part
string json = ExtractJson(allOutput); string json = ExtractJson(allOutput);
if (TryJsonStringDeserialize(json, out T result)) return result;
if (TryJsonStringDeserialize(json, out T runAndCaptureJsonAsync)) return runAndCaptureJsonAsync;
return default; return default;
} }

View File

@ -1,4 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop; using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
@ -46,5 +49,124 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
Assert.AreEqual(true, dto.NfcTagDetected); Assert.AreEqual(true, dto.NfcTagDetected);
Assert.AreEqual(74, dto.ProductType); 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<JsonDataFromPoseidon>(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<JsonDataFromPoseidon>(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<JsonDataFromPoseidon> 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<CliRunner> cliRunnerList = new List<CliRunner>();
for (int i = 0; i < 20; i++)
cliRunnerList.Add(new CliRunner(false));
CliRunner cliRunnerOne = new CliRunner(false);
DateTime StartTime = DateTime.Now;
cliRunnerOne.AddRunAndCaptureJsonAsync<JsonDataFromPoseidon>(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<JsonDataFromPoseidon>(serialPort,
SerialPortData.EMeterArg.AllParams);
for (int i = 0; i < 20; i++)
{
cliRunnerList[iCliRunner].AddRunAndCaptureJsonAsync<JsonDataFromPoseidon>(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<JsonDataFromPoseidon> 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);
}
} }
} }