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:
parent
db43bdb652
commit
c0385c0782
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user