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 : Interface.DASFactory.ICommunication where T : IConnection, new() { /// /// Indicates an error occurred during setup /// http://manuscript.dts.local/f/cases/43289/SW-no-longer-warns-when-hardware-is-in-bootloader-mode /// 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 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 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(SecureQueue.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 /// /// 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 /// 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(); } } /// /// Not currently implemented /// /// /// /// public byte[] SyncExecute(byte[] byteData, int cbTimeout) { throw new NotSupportedException("SyncExecute not supported for DTS.DASLib.Communication.Communication"); } #region Execute /// /// 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. /// /// /// /// /// 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; } } /// /// process the received data /// 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; } } /// /// 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) /// /// 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; } } }