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)
{
/* log = new ScopedLoggerFactory().CreateLogger<CliRunner>(
@"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<CliRunner>(
@"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<string> SendAsync(string fileName, string args)
public async Task<string> 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<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
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<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
{
@ -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;
}

View File

@ -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<CliRunner>(
@"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<CliRunner>(
@"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<string> SendAsync(string fileName, string args)
public async Task<string> 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<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
string allOutput = sb.ToString();
throw;
}
string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? "");
log?.Debug(allOutput);
return allOutput;
}
public void AddRunAndCaptureJsonAsync<T>(SerialPortData data, SerialPortData.EMeterArg eMeterArg) where T : new()
{
@ -123,7 +137,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
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
{
@ -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;
}

View File

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