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.
242 lines
7.7 KiB
C#
242 lines
7.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using log4net;
|
|
using log4net.Config;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using SharedDatabase;
|
|
|
|
namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
|
|
{
|
|
public class CliRunner
|
|
{
|
|
//static readonly ILog log = LogManager.GetLogger(typeof(CliRunner));
|
|
private readonly ILog log;
|
|
private List<Task> taskPool = new List<Task>();
|
|
private long startTime;
|
|
|
|
public List<Task> TaskPool { get { return taskPool; } }
|
|
public void AddTask(Task task) { taskPool.Add(task); }
|
|
public void WaitAll() { Task.WaitAll(taskPool.ToArray()); }
|
|
public void Clear() { taskPool.Clear(); }
|
|
public void CancelAll() { Task.WhenAll(taskPool).ContinueWith(t => { }); }
|
|
|
|
public void StartAll()
|
|
{
|
|
taskPool.ForEach(t => t.Start());
|
|
}
|
|
|
|
public bool AreTasksDone()
|
|
{
|
|
return taskPool.All(task => task.IsCompleted);
|
|
}
|
|
|
|
public long StartTime
|
|
{
|
|
get => startTime;
|
|
}
|
|
|
|
public bool TimeOutReceived(long timeout)
|
|
{
|
|
return (DateTime.Now.Ticks - startTime) > timeout;
|
|
}
|
|
|
|
public CliRunner(bool isCliLogging)
|
|
{
|
|
startTime = DateTime.Now.Ticks;
|
|
|
|
if (isCliLogging)
|
|
{
|
|
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}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="fileName"></param>
|
|
/// <param name="args"></param>
|
|
/// <typeparam name="T"></typeparam>
|
|
public void AddSendAsync(SerialPortData data, SerialPortData.EMeterArg eMeterArg)
|
|
{
|
|
var task = SendAsync(data.SerialPortCmdClientPath, data.DefaultArgSettings(eMeterArg));
|
|
taskPool.Add(task);
|
|
}
|
|
|
|
public void AddSendAsync(string fileName, string args)
|
|
{
|
|
var task = SendAsync(fileName, args);
|
|
taskPool.Add(task);
|
|
}
|
|
|
|
|
|
public async Task<string> SendAsync(string fileName, string args, CancellationToken ct = default)
|
|
{
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = fileName,
|
|
Arguments = args,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
using var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
|
process.Start();
|
|
|
|
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();
|
|
}
|
|
|
|
throw;
|
|
}
|
|
|
|
string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? "");
|
|
log?.Debug(allOutput);
|
|
return allOutput;
|
|
}
|
|
|
|
|
|
public void AddRunAndCaptureJsonAsync<T>(SerialPortData data, SerialPortData.EMeterArg eMeterArg) where T : new()
|
|
{
|
|
var task = RunAndCaptureJsonAsync<T>(data.SerialPortCmdClientPath, data.DefaultArgSettings(eMeterArg));
|
|
taskPool.Add(task);
|
|
}
|
|
|
|
public async Task<T> RunAndCaptureJsonAsync<T>(string fileName, string args, CancellationToken ct = default) where T : new()
|
|
{
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = fileName,
|
|
Arguments = args,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
using var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
|
process.Start();
|
|
|
|
var stdoutTask = process.StandardOutput.ReadToEndAsync();
|
|
var stderrTask = process.StandardError.ReadToEndAsync();
|
|
|
|
await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct));
|
|
|
|
string allOutput = (stdoutTask.Result ?? "") + (stderrTask.Result ?? "");
|
|
log?.Debug(allOutput);
|
|
|
|
string json = ExtractJson(allOutput);
|
|
if (TryJsonStringDeserialize(json, out T result)) return result;
|
|
return default;
|
|
}
|
|
|
|
public bool TryJsonStringDeserialize<T>(string json, out T runAndCaptureJsonAsync) where T : new()
|
|
{
|
|
if (json != null)
|
|
{
|
|
try
|
|
{
|
|
runAndCaptureJsonAsync = JsonConvert.DeserializeObject<T>(json);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Debug(ex.Message);
|
|
runAndCaptureJsonAsync = TryConvert<T>(json);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
runAndCaptureJsonAsync = default;
|
|
return false;
|
|
}
|
|
|
|
|
|
private static T TryConvert<T>(string json) where T : new()
|
|
{
|
|
|
|
T obj = new T();
|
|
|
|
try
|
|
{
|
|
JObject jObject = JObject.Parse(json);
|
|
|
|
foreach (PropertyInfo prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
if (!prop.CanWrite) continue;
|
|
|
|
JToken token;
|
|
if (jObject.TryGetValue(prop.Name, StringComparison.OrdinalIgnoreCase, out token))
|
|
{
|
|
try
|
|
{
|
|
object value = token.ToObject(prop.PropertyType);
|
|
prop.SetValue(obj, value);
|
|
}
|
|
catch
|
|
{
|
|
// leave default if conversion fails
|
|
}
|
|
}
|
|
// else → keep default value
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"TryConvert failed: {ex.Message}");
|
|
}
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
public string ExtractJson(string text)
|
|
{
|
|
int start = text.IndexOf('{');
|
|
int end = text.LastIndexOf('}');
|
|
|
|
if (start >= 0 && end > start)
|
|
{
|
|
return text.Substring(start, end - start + 1);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
} |