492 lines
15 KiB
C#
492 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using log4net;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
|
|
{
|
|
public class CliRunner
|
|
{
|
|
private readonly ILog log;
|
|
private readonly List<CliTaskInfo> taskPool = new List<CliTaskInfo>();
|
|
private long startTimeMs;
|
|
private long incommingTimeMs;
|
|
|
|
public List<CliTaskInfo> TaskPool
|
|
{
|
|
get { return taskPool; }
|
|
}
|
|
|
|
public long StartTime
|
|
{
|
|
get { return startTimeMs; }
|
|
}
|
|
|
|
public long IncommingTime
|
|
{
|
|
get { return incommingTimeMs; }
|
|
}
|
|
|
|
public CliRunner(bool isCliLogging)
|
|
{
|
|
ResetStartTime();
|
|
|
|
if (isCliLogging)
|
|
{
|
|
try
|
|
{
|
|
log = new ScopedLoggerFactory().CreateLogger<CliRunner>(
|
|
@"C:\TBF\Logs\CliRunner.txt",
|
|
10,
|
|
7,
|
|
log4net.Core.Level.Debug,
|
|
true,
|
|
true,
|
|
TimeSpan.FromMinutes(2)
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Failed to initialize logger: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ResetStartTime()
|
|
{
|
|
startTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
foreach (var item in taskPool)
|
|
{
|
|
try
|
|
{
|
|
item?.Cts?.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
try
|
|
{
|
|
if (item?.Process != null)
|
|
{
|
|
item.Process.Dispose();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
taskPool.Clear();
|
|
}
|
|
|
|
public void WaitAll()
|
|
{
|
|
var tasks = taskPool
|
|
.Where(t => t != null && t.Task != null)
|
|
.Select(t => t.Task)
|
|
.ToArray();
|
|
|
|
if (tasks.Length == 0)
|
|
return;
|
|
|
|
try
|
|
{
|
|
Task.WaitAll(tasks);
|
|
}
|
|
catch (AggregateException)
|
|
{
|
|
RefreshTaskStates();
|
|
}
|
|
}
|
|
|
|
public bool AreTasksDone()
|
|
{
|
|
RefreshTaskStates();
|
|
return taskPool.Count > 0 && taskPool.All(t => t.State != CliTaskState.Running);
|
|
}
|
|
|
|
public bool TimeOutReceived(long timeoutMs)
|
|
{
|
|
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
return (nowMs - startTimeMs) > timeoutMs;
|
|
}
|
|
|
|
public void RefreshTaskStates()
|
|
{
|
|
foreach (var item in taskPool)
|
|
{
|
|
if (item == null || item.Task == null)
|
|
continue;
|
|
|
|
if (item.State == CliTaskState.TimedOut)
|
|
continue;
|
|
|
|
if (!item.Task.IsCompleted)
|
|
{
|
|
item.State = CliTaskState.Running;
|
|
}
|
|
else if (item.Task.IsCanceled)
|
|
{
|
|
item.State = CliTaskState.Canceled;
|
|
}
|
|
else if (item.Task.IsFaulted)
|
|
{
|
|
item.State = CliTaskState.Faulted;
|
|
}
|
|
else
|
|
{
|
|
item.State = CliTaskState.Completed;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void CancelUndoneTasksAsTimedOut()
|
|
{
|
|
foreach (var item in taskPool)
|
|
{
|
|
if (item == null || item.Task == null)
|
|
continue;
|
|
|
|
if (item.Task.IsCompleted)
|
|
{
|
|
if (item.Task.IsCanceled)
|
|
item.State = CliTaskState.Canceled;
|
|
else if (item.Task.IsFaulted)
|
|
item.State = CliTaskState.Faulted;
|
|
else
|
|
item.State = CliTaskState.Completed;
|
|
|
|
continue;
|
|
}
|
|
|
|
item.State = CliTaskState.TimedOut;
|
|
|
|
try
|
|
{
|
|
item.Cts?.Cancel();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log?.Warn("Cancel token failed", ex);
|
|
}
|
|
|
|
try
|
|
{
|
|
if (item.Process != null && !item.Process.HasExited)
|
|
{
|
|
item.Process.Kill();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log?.Warn("Kill process failed", ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void AddSendAsync(SerialPortData data, SerialPortData.EMeterArg eMeterArg)
|
|
{
|
|
AddSendAsync(data.SerialPortCmdClientPath, data.DefaultArgSettings(eMeterArg));
|
|
}
|
|
|
|
public void AddSendAsync(string fileName, string args)
|
|
{
|
|
ResetStartTime();
|
|
|
|
var cts = new CancellationTokenSource();
|
|
var info = new CliTaskInfo
|
|
{
|
|
Cts = cts,
|
|
Name = "SendAsync"
|
|
};
|
|
|
|
var task = SendAsync(fileName, args, info, cts.Token);
|
|
info.Task = task;
|
|
taskPool.Add(info);
|
|
}
|
|
|
|
public async Task<string> SendAsync(string fileName, string args, CliTaskInfo info, CancellationToken ct = default)
|
|
{
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = fileName,
|
|
Arguments = args,
|
|
WorkingDirectory = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(fileName)),
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
using (var process = new Process { StartInfo = psi, EnableRaisingEvents = true })
|
|
{
|
|
info.Process = process;
|
|
|
|
try
|
|
{
|
|
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 (info.State != CliTaskState.TimedOut)
|
|
info.State = CliTaskState.Canceled;
|
|
|
|
if (!process.HasExited)
|
|
{
|
|
process.Kill();
|
|
}
|
|
|
|
throw;
|
|
}
|
|
|
|
incommingTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
|
|
info.ExitCode = process.ExitCode;
|
|
info.StandardOutput = stdOutTask.Result ?? "";
|
|
info.StandardError = stdErrTask.Result ?? "";
|
|
string allOutput = info.StandardOutput + info.StandardError;
|
|
log?.Debug(allOutput);
|
|
|
|
if (info.ExitCode != 0)
|
|
{
|
|
info.FailureReason = $"CLI exited with exit code {info.ExitCode}.";
|
|
log?.Error($"{info.Name}: {info.FailureReason} stderr='{info.StandardError}'");
|
|
}
|
|
|
|
info.State = CliTaskState.Completed;
|
|
return allOutput;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
info.State = CliTaskState.Faulted;
|
|
log?.Error("SendAsync failed", ex);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
info.Process = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void AddRunAndCaptureJsonAsync<T>(SerialPortData data, SerialPortData.EMeterArg eMeterArg) where T : new()
|
|
{
|
|
AddRunAndCaptureJsonAsync<T>(
|
|
data.SerialPortCmdClientPath,
|
|
data.DefaultArgSettings(eMeterArg));
|
|
}
|
|
|
|
public void AddRunAndCaptureJsonAsync<T>(string fileName, string args) where T : new()
|
|
{
|
|
ResetStartTime();
|
|
|
|
var cts = new CancellationTokenSource();
|
|
var info = new CliTaskInfo
|
|
{
|
|
Cts = cts,
|
|
Name = $"RunAndCaptureJsonAsync<{typeof(T).Name}>"
|
|
};
|
|
|
|
var task = RunAndCaptureJsonAsync<T>(fileName, args, info, cts.Token);
|
|
|
|
info.Task = task;
|
|
taskPool.Add(info);
|
|
}
|
|
|
|
public async Task<T> RunAndCaptureJsonAsync<T>(string fileName, string args, CliTaskInfo info, CancellationToken ct = default) where T : new()
|
|
{
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = fileName,
|
|
Arguments = args,
|
|
WorkingDirectory = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(fileName)),
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
using (var process = new Process { StartInfo = psi, EnableRaisingEvents = true })
|
|
{
|
|
info.Process = process;
|
|
|
|
try
|
|
{
|
|
process.Start();
|
|
|
|
var stdoutTask = process.StandardOutput.ReadToEndAsync();
|
|
var stderrTask = process.StandardError.ReadToEndAsync();
|
|
|
|
try
|
|
{
|
|
await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct));
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
if (info.State != CliTaskState.TimedOut)
|
|
info.State = CliTaskState.Canceled;
|
|
|
|
if (!process.HasExited)
|
|
{
|
|
process.Kill();
|
|
}
|
|
|
|
throw;
|
|
}
|
|
|
|
incommingTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
|
|
info.ExitCode = process.ExitCode;
|
|
info.StandardOutput = stdoutTask.Result ?? "";
|
|
info.StandardError = stderrTask.Result ?? "";
|
|
string allOutput = info.StandardOutput + info.StandardError;
|
|
log?.Debug(allOutput);
|
|
|
|
if (info.ExitCode != 0)
|
|
{
|
|
info.FailureReason = $"CLI exited with exit code {info.ExitCode}.";
|
|
log?.Error($"{info.Name}: {info.FailureReason} stderr='{info.StandardError}'");
|
|
info.State = CliTaskState.Completed;
|
|
return default(T);
|
|
}
|
|
|
|
string json = ExtractJson(allOutput);
|
|
T result;
|
|
if (TryJsonStringDeserialize(json, out result))
|
|
{
|
|
info.State = CliTaskState.Completed;
|
|
return result;
|
|
}
|
|
|
|
info.FailureReason = "CLI completed without a valid JSON response.";
|
|
log?.Error($"{info.Name}: {info.FailureReason} stdout='{info.StandardOutput}' stderr='{info.StandardError}'");
|
|
info.State = CliTaskState.Completed;
|
|
return default(T);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
info.State = CliTaskState.Faulted;
|
|
log?.Error("RunAndCaptureJsonAsync failed", ex);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
info.Process = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool TryJsonStringDeserialize<T>(string json, out T value) where T : new()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(json))
|
|
{
|
|
try
|
|
{
|
|
value = JsonConvert.DeserializeObject<T>(json);
|
|
return value != null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log?.Debug(ex.Message);
|
|
return TryConvert(json, out value);
|
|
}
|
|
}
|
|
|
|
value = default(T);
|
|
return false;
|
|
}
|
|
|
|
private static bool TryConvert<T>(string json, out T value) 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 propertyValue = token.ToObject(prop.PropertyType);
|
|
prop.SetValue(obj, propertyValue);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
value = obj;
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"TryConvert failed: {ex.Message}");
|
|
value = default(T);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
internal void AddTaskForTest(Task task, string name = "TestTask", CancellationTokenSource cts = null)
|
|
{
|
|
taskPool.Add(new CliTaskInfo
|
|
{
|
|
Task = task,
|
|
Cts = cts,
|
|
Name = name,
|
|
State = task.IsCompleted
|
|
? (task.IsCanceled ? CliTaskState.Canceled :
|
|
task.IsFaulted ? CliTaskState.Faulted :
|
|
CliTaskState.Completed)
|
|
: CliTaskState.Running
|
|
});
|
|
}
|
|
}
|
|
}
|