Files
DP44/Common/DTS.Common.ICommunication/Communication.cs
2026-04-17 14:55:32 -04:00

1016 lines
34 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;
using DTS.Common.Utilities.Logging;
using System.Net.Sockets;
using DTS.Common.Utilities;
using DTS.Common.Utils;
using static DTS.Common.Enums.Communication.CommunicationConstantsAndEnums;
using DTS.Common.Interface.Communication;
using DTS.Common.Interface.Connection;
using DTS.Common.Classes.Connection;
using DTS.Common.Enums.DASFactory;
using System.Threading.Tasks;
namespace DTS.Common.ICommunication
{
public abstract class Communication<T> : Interface.DASFactory.ICommunication where T : IConnection, new()
{
/// <summary>
/// Indicates an error occurred during setup
/// http://manuscript.dts.local/f/cases/43289/SW-no-longer-warns-when-hardware-is-in-bootloader-mode
/// </summary>
public bool ErrorInSetup { get; set; } = false;
protected void Lock() { }
protected void Release() { }
protected T sock;
public string ConnectString => sock.ConnectString;
public bool Connected => sock.Connected;
public int CompareTo(Interface.DASFactory.ICommunication dev)
{
return string.Compare(sock.ConnectString.ToUpper(), dev.ConnectString.ToUpper(), StringComparison.Ordinal);
}
public int CompareTo(string conStr)
{
return string.Compare(sock.ConnectString.ToUpper(), conStr.ToUpper(), StringComparison.Ordinal);
}
public ICommunication_DASInfo DASInfo { get; set; }
protected SecureQueue<byte> SockDataBuffer;
protected ManualResetEvent SyncEvent;
public IConnection Transport
{
get => sock;
set => throw new NotSupportedException("Communication::Transport Can't replace socket");
}
public int ReceiveBufferSize { get; set; }
public string SerialNumber { get; set; }
private string _firmwareVersion;
public string FirmwareVersion
{
get => _firmwareVersion;
set
{
_firmwareVersion = value;
FirmwareVersionUpdated();
}
}
protected virtual void FirmwareVersionUpdated()
{
}
public byte ProtocolVersion { get; set; }
public Dictionary<DFConstantsAndEnums.ProtocolLimitedCommands, byte> MinimumProtocols { get; set; }
public abstract void InitMinProto();
public bool IsCommandSupported(DFConstantsAndEnums.ProtocolLimitedCommands command)
{
return ProtocolVersion >= GetMinProto(command);
// Not Supported
}
public byte GetMinProto(DFConstantsAndEnums.ProtocolLimitedCommands command)
{
if (null == MinimumProtocols) { throw new NotSupportedException("GetMinProto MinimumProtocols is null"); }
return !MinimumProtocols.ContainsKey(command) ? Convert.ToByte(0xFF) : Convert.ToByte(MinimumProtocols[command]);
}
public int FlushTimeoutMilliSec { get; set; }
#region Action data
public abstract class ActionData : IDisposable
{
protected readonly CommunicationCallback UserCallback;
protected readonly object UserCallbackObject;
public readonly int UserTimeout;
protected ActionData(CommunicationCallback cb, object obj, int to)
{
Debug.Assert(cb != null, "ActionData: callback can't be null");
UserCallback = cb;
UserCallbackObject = obj;
UserTimeout = to;
}
public void Dispose()
{
KillTimer();
}
#region Timeout timer
private Timer _callbackTimer;
private bool _timerFired;
private object _timerLock;
public void StartTimer()
{
_timerLock = new object();
_timerFired = false;
_callbackTimer = new Timer(TimerCallback, this, UserTimeout, Timeout.Infinite);
}
private void TimerCallback(object state)
{
lock (_timerLock)
{
if (_callbackTimer == null || _timerFired)
{
return;
}
// remember that we where here
_timerFired = true;
// stop it from happening again
_callbackTimer.Change(Timeout.Infinite, Timeout.Infinite);
_callbackTimer.Dispose();
_callbackTimer = null;
// tell user we timed out
var sr = new CommunicationReport(UserCallbackObject, GetTimeoutResult());
UserCallback(sr);
}
}
public void KillTimer()
{
lock (_timerLock)
{
if (_callbackTimer == null || _timerFired)
{
return;
}
try
{
_callbackTimer.Change(Timeout.Infinite, Timeout.Infinite);
_callbackTimer.Dispose();
_callbackTimer = null;
}
catch
{
// just to kill a stupid message...
}
}
}
public bool TimerExpired => _timerFired;
#endregion
public bool ReportCanceled()
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.Canceled);
return UserCallback(sr);
}
public abstract bool ReportFailure();
public abstract bool ReportSuccess();
protected abstract CommunicationResult GetTimeoutResult();
}
private class ConnectActionData : ActionData
{
public ConnectActionData(CommunicationCallback cb, object obj, int to)
: base(cb, obj, to)
{
}
public override bool ReportFailure()
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.ConnectFailed);
return UserCallback(sr);
}
public override bool ReportSuccess()
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.ConnectOK);
return UserCallback(sr);
}
protected override CommunicationResult GetTimeoutResult()
{
return CommunicationResult.ConnectTimeout;
}
}
private class DisconnectActionData : ActionData
{
public DisconnectActionData(CommunicationCallback cb, object obj, int to)
: base(cb, obj, to)
{
}
public override bool ReportFailure()
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.DisconnectFailed);
return UserCallback(sr);
}
public override bool ReportSuccess()
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.DisconnectOK);
return UserCallback(sr);
}
protected override CommunicationResult GetTimeoutResult()
{
return CommunicationResult.DisconnectTimeout;
}
}
public class ExecuteActionData : ActionData
{
public ExecuteActionData(CommunicationCallback cb, object obj, int to)
: base(cb, obj, to)
{
}
public override bool ReportFailure()
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.SendFailed);
return UserCallback(sr);
}
public override bool ReportSuccess()
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.SendOK);
return UserCallback(sr);
}
public bool ReportReadSuccess(byte[] data)
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.ReceiveOK);
sr.Data = data;
return UserCallback(sr);
}
public bool ReportReadTimeout()
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.ReceiveTimeout);
return UserCallback(sr);
}
public bool ReportReadDisconnected()
{
if (UserCallback == null) return false;
var sr = new CommunicationReport(UserCallbackObject, CommunicationResult.ReceiveFailed);
return UserCallback(sr);
}
protected override CommunicationResult GetTimeoutResult()
{
return CommunicationResult.SendTimeout;
}
}
#endregion
public event EventHandler OnDisconnected;
#region Execution locking
private readonly object _executeBusyMtLock = new object();
private bool _executeIsBusy;
public bool ExecuteIsBusy
{
get
{
lock (_executeBusyMtLock)
{
return _executeIsBusy;
}
}
set
{
// protected against multi-threaded entry
lock (_executeBusyMtLock)
{
if (value)
{
// caller trying to lock resource
if (_executeIsBusy)
{
// but it's already locked
Debug.Assert(false, "ExecuteIsBusy: re-entry");
}
_executeIsBusy = true;
}
else
{
// caller is leaving
if (!_executeIsBusy)
{
// but it isn't locked
Debug.Assert(false, "ExecuteIsBusy: extra unlock");
}
_executeIsBusy = false;
}
}
}
}
#endregion
public Communication()
{
_disposed = false;
sock = new T();
sock.OnDisconnected += sock_OnDisconnected;
SockDataBuffer = new SecureQueue<byte>(SecureQueue<byte>.NullPolicy.SkipNull, "Communication.SockDataBuffer");
FlushTimeoutMilliSec = 5;
ReceiveBufferSize = (int)Math.Pow(2, 16);
SyncEvent = new ManualResetEvent(false);
InitMinProto();
}
public void Close(int timeout)
{
}
private void sock_OnDisconnected(object sender, EventArgs e)
{
OnDisconnected?.Invoke(this, null);
}
#region Exit handling
~Communication()
{
//AddTrace("~Communication");
try
{
Dispose(false);
}
catch (Exception)
{
}
}
public void Dispose()
{
//AddTrace("Communication.Dispose");
Dispose(true);
GC.SuppressFinalize(this);
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
if (sock != null)
{
sock.Dispose();
}
SockDataBuffer?.Dispose();
}
_disposed = true;
}
#endregion
#region Connect
public void Connect(string connectString, CommunicationCallback callback, object callbackObject, int callbackTimeout, string hostIPAddress)
{
if (sock == null || sock.Connected)
{
throw new Exception("Communication.Connect: Socket not initialized or already connected");
}
var action = new ConnectActionData(callback, callbackObject, callbackTimeout);
try
{
sock.Create(connectString, hostIPAddress);
action.StartTimer();
sock.BeginConnect(ConnectCallback, action);
}
catch
{
action.Dispose();
throw;
}
}
private void ConnectCallback(IAsyncResult ar)
{
var action = ar.AsyncState as ActionData;
try
{
// Complete the connection.
sock.EndConnect(ar);
action?.KillTimer();
}
catch (Exception ex)
{
APILogger.Log($"ConnectCallback failed - {sock.ConnectString}", ex);
action?.KillTimer();
if (null != action && !action.TimerExpired)
{
// no, we're fine.
action.ReportFailure();
}
return;
}
// check if we're too late
if (null == action || action.TimerExpired) return;
// no, we're fine. Now setup our reader
SetupReader();
action.ReportSuccess();
}
#endregion
#region Disconnect
public void Disconnect(bool reuseSocket,
CommunicationCallback callback,
object callbackObject,
int callbackTimeout)
{
if (sock == null)
{
throw new Exception("Communication.Disconnect: Socket not initialized or already disconnected");
}
CancelEvent.Set();
var action = new DisconnectActionData(callback, callbackObject, callbackTimeout);
try
{
action.StartTimer();
sock.BeginDisconnect(reuseSocket, DisconnectCallback, action);
}
catch
{
action.Dispose();
throw;
}
}
private void DisconnectCallback(IAsyncResult ar)
{
CancelEvent.Set();
var action = ar.AsyncState as ActionData;
try
{
// Complete the disconnect.
sock.EndDisconnect(ar);
action?.KillTimer();
}
catch
{
action?.KillTimer();
// check if we're too late
if (null != action && !action.TimerExpired)
{
// no, we're fine.
action.ReportFailure();
}
return;
}
// check if we're too late
if (null != action && !action.TimerExpired)
{
// no, we're fine.
action.ReportSuccess();
}
}
#endregion
#region Cancel functions
/// <summary>
/// event that will signal if cancel has happened
/// prior to this IsCanceled would have to be checked
/// or polled
/// 17600 Communication class improvements from 3.2
/// </summary>
public ManualResetEvent CancelEvent { get; } = new ManualResetEvent(false);
private bool _commandsAreCanceled;
public void ForceCancel()
{
_commandsAreCanceled = true;
_sleepAmount = 250;
CancelEvent.Set();
}
public void Cancel()
{
_commandsAreCanceled = true;
_sleepAmount = 500;
CancelEvent.Set();
}
public bool IsCanceled()
{
return _commandsAreCanceled;
}
private int _sleepAmount = 500;
public void ClearCancel()
{
if (!_commandsAreCanceled) return;
Thread.Sleep(_sleepAmount);
APILogger.LogString($"Communication.ClearCancel: Waited {_sleepAmount}ms, clearing");
_commandsAreCanceled = false;
CancelEvent.Reset();
}
#endregion
public void Flush(int timeoutMs)
{
if (DFConstantsAndEnums.ExtraCommunicationLogging)
{
APILogger.Log($"Flushing {sock.ConnectString}");
}
SockDataBuffer.Flush();
while (SockDataBuffer.WaitForData(timeoutMs))
{
SockDataBuffer.Flush();
}
}
/// <summary>
/// Not currently implemented
/// </summary>
/// <param name="byteData"></param>
/// <param name="cbTimeout"></param>
/// <returns></returns>
public byte[] SyncExecute(byte[] byteData, int cbTimeout)
{
throw new NotSupportedException("SyncExecute not supported for DTS.DASLib.Communication.Communication");
}
#region Execute
/// <summary>
/// pseudo execute is for commands that don't need to send any data and just receive data.
/// an example of this is the G5 MonitorData command
///
/// this execute should be used in the place of a normal execute command, it will not clear out the buffer and will pick up form wherever the
/// previous pseudoexecute leaves off.
/// </summary>
/// <param name="byteData"></param>
/// <param name="cb"></param>
/// <param name="cbObject"></param>
/// <param name="cbTimeout"></param>
public void PseudoExecute(byte[] byteData, CommunicationCallback cb, object cbObject, int cbTimeout)
{
if (IsCanceled())
{
throw new CanceledException();
}
if (!Connected)
{
throw new NotConnectedException(DASResource.Strings.Communication_Execute_Err1);
}
var action = new ExecuteActionData(cb, cbObject, cbTimeout);
try
{
// since we aren't sending anything, we can go straight to the callback
PseudoSRSendCallback(new PseudoAsyncResult(action));
}
catch
{
action.Dispose();
throw;
}
}
/// <summary>
/// process the received data
/// </summary>
private void ProcessReceivedData(ExecuteActionData action, int timeout)
{
try
{
while (true)
{
bool bufferHasData;
try
{
_beforeId = Thread.CurrentThread.ManagedThreadId;
bufferHasData = WaitWithCondition.Wait(SockDataBuffer.QueueWaitHandle, timeout, CancelEvent);
var afterId = Thread.CurrentThread.ManagedThreadId;
if (afterId != _beforeId)
{
// we shouldn't be here
APILogger.LogString("Communication.ProcessReceivedData: Before and after ID doesn't match, exiting");
return;
}
}
catch (WaitWithCondition.ConditionMetException)
{
if (!sock.Connected || !_receiveCallbackRunning)
{
APILogger.LogString("Communication.ProcessReceivedData: recorder is not connected");
action?.ReportReadDisconnected();
sock_OnDisconnected(this, null);
}
else
{
APILogger.LogString("Communication.ProcessReceivedData: WaitWithCancel threw a CanceledException");
action?.ReportCanceled();
}
return;
}
if (!bufferHasData)
{
// timeout!
action.ReportReadTimeout();
return;
}
// we're signaled
var buffer = SockDataBuffer.Dequeue(true);
if (buffer == null || buffer.Length <= 0) continue;
try
{
if (DFConstantsAndEnums.ExtraCommunicationLogging)
{
APILogger.Log("received ", buffer);
}
}
catch (Exception)
{
}
// the buffer has data
if (action.ReportReadSuccess(buffer))
{
// they want more
}
else
{
return;
}
}
}
catch (Exception ex)
{
APILogger.Log("MessageBox", DASResource.Strings.Communication_Execute, ex);
// Execution stops here, but I don't think I want to set not busy here
}
}
public void Execute(byte[] byteData, CommunicationCallback cb, object cbObject, int cbTimeout)
{
if (IsCanceled())
{
throw new CanceledException();
}
if (!Connected)
{
throw new NotConnectedException(DASResource.Strings.Communication_Execute_Err1);
}
CancelEvent.Reset();
var action = new ExecuteActionData(cb, cbObject, cbTimeout);
//FB 25582 Refactored to reduce method Cognitive Complexity
CheckAndFlushBuffer(action);
Task sendAsyncTask = SendDataToDevice(byteData, action);
ProcessData(cbTimeout, action, sendAsyncTask);
}
private void ProcessData(int cbTimeout, ExecuteActionData action, Task sendAsyncTask)
{
// Continue if the task was successful, if there is a timeout/ error in send task this code would not get called.
sendAsyncTask.ContinueWith(t =>
{
if (null != action && !action.TimerExpired)
{
//process the received data
ProcessReceivedData(action, cbTimeout);
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
// FB 18389 log the exception
// Continue if the task was faulted
sendAsyncTask.ContinueWith(t =>
{
if (t.Exception != null)
{
foreach (var e in t.Exception.Flatten().InnerExceptions)
{
APILogger.Log("Exception in sendAsyncTask ", e.ToString());
}
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
private Task SendDataToDevice(byte[] byteData, ExecuteActionData action)
{
//Sending the data to the remote device.
return Task.Run(async () =>
{
try
{
//https://johnthiriet.com/configure-await/
var t = sock.SendAsync(byteData, 0, byteData.Length).ConfigureAwait(false);
action?.StartTimer();
await t;
action?.KillTimer();
if (IsCanceled())
{
action?.ReportCanceled();
}
}
catch (Exception e)
{
try
{
action?.KillTimer();
APILogger.LogString("Communication.Execute: sendAsyncTask threw an exception");
APILogger.LogException(e);
if (IsCanceled())
{
action?.ReportCanceled();
}
else
{
action?.ReportFailure();
}
}
catch (Exception ex)
{
APILogger.LogString("Communication.Execute: Exception handler threw exception!");
APILogger.LogException(ex);
throw;
// Execution stops here, but I don't think I want to set not busy here
}
throw;
}
});
}
private void CheckAndFlushBuffer(ExecuteActionData action)
{
try
{
// make sure we don't have anything pending
if (!SockDataBuffer.IsEmpty())
{
if (DFConstantsAndEnums.ExtraCommunicationLogging)
{
APILogger.Log($"Flushing {sock.ConnectString}");
}
Flush(FlushTimeoutMilliSec);
}
}
catch
{
action.Dispose();
throw;
}
}
/// <summary>
/// This is the callback for the PseudoExecute function
/// this is identical to SRSendCallback with the exception that it
/// doesn't do sock.endsend (as nothing was sent)
/// </summary>
/// <param name="ar"></param>
private void PseudoSRSendCallback(IAsyncResult ar)
{
var action = ar.AsyncState as ExecuteActionData;
try
{
SRRecvCallback(action);
}
catch (Exception)
{
try
{
action?.ReportFailure();
}
catch (Exception exx)
{
APILogger.LogString("Communication.SRSendCallback: Exception handler threw exception!");
APILogger.LogException(exx);
// Execution stops here, but I don't think I want to set not busy here
}
}
}
private int _beforeId;
private void SRRecvCallback(object _action)
{
try
{
var action = _action as ExecuteActionData;
while (true)
{
bool bufferHasData;
try
{
_beforeId = Thread.CurrentThread.ManagedThreadId;
bufferHasData = WaitWithCondition.Wait(SockDataBuffer.QueueWaitHandle, action.UserTimeout, CancelEvent);
var afterId = Thread.CurrentThread.ManagedThreadId;
if (afterId != _beforeId)
{
// we shouldn't be here
APILogger.LogString("Communication.SRRecvCallback: Before and after ID doesn't match, exiting");
return;
}
}
catch (WaitWithCondition.ConditionMetException)
{
if (!sock.Connected || !_receiveCallbackRunning)
{
APILogger.LogString("Communication.SRRecvCallback: recorder is not connected");
action?.ReportReadDisconnected();
sock_OnDisconnected(this, null);
}
else
{
APILogger.LogString("Communication.SRRecvCallback: WaitWithCancel threw a CanceledException");
action?.ReportCanceled();
}
return;
}
if (!bufferHasData)
{
// timeout!
action.ReportReadTimeout();
return;
}
// we're signaled
var buffer = SockDataBuffer.Dequeue(true);
if (buffer == null || buffer.Length <= 0) continue;
// the buffer has data
if (action.ReportReadSuccess(buffer))
{
// they want more
}
else
{
return;
}
}
}
catch (Exception ex)
{
APILogger.Log("MessageBox", DASResource.Strings.Communication_SRRecvCallbackFailed, ex);
// Execution stops here, but I don't think I want to set not busy here
}
}
#endregion
#region Reading thread
public void SetupReader()
{
var receiveDataBuffer = new byte[ReceiveBufferSize];
sock.BeginReceive(receiveDataBuffer, 0, ReceiveBufferSize,
ReceiveCallback, receiveDataBuffer);
}
private bool _receiveCallbackRunning = true;
// callback for Receive
private void ReceiveCallback(IAsyncResult ar)
{
try
{
var bytesRead = 0;
byte[] receiveBuffer = null;
if (null == sock || !sock.Connected)
{
}
else
{
// retrieve buffer
receiveBuffer = ar.AsyncState as byte[];
// Read data from the remote device.
bytesRead = sock.EndReceive(ar);
}
//APILogger.LogString("ReceiveCallback: " + this.ToString() + " got " + bytesRead.ToString());
if (bytesRead > 0)
{
var tmp = new byte[bytesRead];
Buffer.BlockCopy(receiveBuffer, 0, tmp, 0, bytesRead);
SockDataBuffer.Enqueue(tmp);
// Get the rest of the data.
sock.BeginReceive(receiveBuffer, 0, ReceiveBufferSize,
ReceiveCallback, receiveBuffer);
}
else
{
// we're disconnected
APILogger.LogString($"Communication.ReceiveCallback: sock.EndReceive returned 0, exiting, {this?.ConnectString}: Connected:{sock?.Connected}");
_receiveCallbackRunning = false;
// REPORT this as a disconnect!
// http://fogbugz/fogbugz/default.asp?14728 - Not notified if you lose a single slice6 on a slice6db
OnDisconnected?.Invoke(this, null);
}
}
catch (Exception ex)
{
if (ex is SocketException)
{
var connectionString = "N/A";
if (sock != null && sock.ConnectString != null)
{
connectionString = sock.ConnectString;
}
APILogger.LogString(
$"{APILogger.GetCurrentMethod()} Connection:{connectionString} Message:{ex.Message}");
sock.KeepAliveErrorReceived();
}
else
{
APILogger.LogException(ex);
}
_receiveCallbackRunning = false;
}
}
#endregion
public class PseudoAsyncResult : IAsyncResult
{
private readonly ExecuteActionData _action;
public PseudoAsyncResult(ExecuteActionData action)
{
_action = action;
}
public bool IsCompleted => true;
public WaitHandle AsyncWaitHandle => throw new NotImplementedException();
public object AsyncState => _action;
public bool CompletedSynchronously => true;
}
}
public class CommunicationReport : ICommunicationReport
{
public object UserState { get; set; }
public CommunicationResult Result { get; set; }
public byte[] Data { get; set; }
public CommunicationReport(object state, CommunicationResult res)
{
UserState = state;
Result = res;
}
}
}