using System; using System.Linq; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using JD.Core; using JD.Core.ProjectManaging; using JD.Interfaces.Remoting; using JD.Remoting.Details; using JD.Remoting.Exceptions; using JetBrains.Annotations; namespace JD.Remoting.Connections { public abstract class TcpConnection : IConnection, IDisposable { public static IPlugin NoPlugin = new NoPlugin(); protected readonly IPlugin _plugin; private const string UnknownPlugin = "unknownPlugin"; /// /// Gets the size of the buffer used by the JD.Remoting.TCPConnection.Receive() helper function. /// private const int BufferSize = 8192; private readonly Task m_connectionTask; private CancellationTokenSource m_cancellationTokenSource; protected Socket m_socket = null; protected Mutex m_socketMutex = null; protected EndPoint m_endPoint = null; protected IEndPointId m_endPointId; protected bool m_disposed = false; private readonly string m_pluginName = UnknownPlugin; private readonly byte[] m_receiveBuffer; private readonly MessageBuilder m_messageBuilder; private bool m_autoConnect = true; /// /// Default constructor. /// protected TcpConnection([NotNull]IPlugin plugin) { _plugin = plugin; m_pluginName = plugin?.Name ?? UnknownPlugin; m_socketMutex = new Mutex(); m_messageBuilder = new MessageBuilder(); m_receiveBuffer = new byte[BufferSize]; m_cancellationTokenSource = new CancellationTokenSource(); m_connectionTask = TaskManager.Instance.CreateTask(ConnectionTask, m_cancellationTokenSource.Token); m_connectionTask.Start(); } #region IConnection members public string PluginName { get { return m_pluginName; } } public IEndPointId EndPoint { get { return m_endPointId; } } public string ProjectId { get; private set; } /// /// Opens a TCP connection. /// /// Zero if succeeded, a non-zero value otherwise. public virtual int Open(IEndPointId endPointId) { m_autoConnect = true; return 0; } /// /// Closes the connection. /// /// Always zero. public virtual int Close() { m_autoConnect = false; Disconnect(); return 0; } /// /// Sends a message to the host connected. /// /// A message to send. /// 0 if operation succeeded, a non-zero value otherwise. /// See Windows Sockets Error Codes page for more information on error codes. /// The System.Net.Socket has been closed. public int Send(IMessage message) { return Send(new MessageHeader { CommandName = message.CommandName }, message.Content); } private int Send(MessageHeader messageHeader, IBlob content = null) { content = content ?? Blob.Empty; int errorCode = 0; if (string.IsNullOrEmpty(messageHeader.PluginName)) { messageHeader.PluginName = PluginName; } byte[][] packets = PacketBuilder.BuildPackets(messageHeader, content).ToArray(); using (new AutoMutex(m_socketMutex)) { foreach (byte[] data in packets) { int bytesSent = 0; try { do { bytesSent += Send(data); } while (bytesSent != data.Length); } catch (SocketException sException) { errorCode = sException.ErrorCode; } } } return errorCode; } protected virtual int Send(byte[] data) { return m_socket.Send(data); } /// /// Occurs regularly. Meant to be used to send some messages asynchronously, but can be used as you wish. /// In some implementations, the Update event may not be called regularly. /// As the Update event is linked to the Receive event, if the thread loop blocks until a connection is received, /// it may take some time to receive this message. /// The currently affected implementations are : /// - JD.Remoting.Connections.TCPServerConnection. /// Do not use this event for time-critical operations when using these implementations. /// /// public event EventHandler Update; /// /// Occurs when a message is received. /// The command must handle the message contents immediately, as the message contents will be disposed /// just after the Received event was triggered. /// If you can't, create a blob clone with the JD.Remoting.Blob.Clone function and use this copy to complete your operations. /// /// This event may not be called if a binding command is set. public event EventHandler Received; /// /// Triggered when the connection state changes. /// public event EventHandler ConnectedChanged; /// /// Triggered when the connection target is not compatible with the current connection. /// /// You can try to continue execution anyway. In this case, you can ignore this message. /// Otherwise, set the Cancel property of the event argument to true. This will force a disconnection /// and trigger the ConnectedChanged event. /// /// public event System.ComponentModel.CancelEventHandler ConnectedToIncompatibleTarget; /// /// Triggered when the connection target could not find a valid engine branch specification, but did use /// the default specification. /// /// You can try to continue execution anyway. In this case, you can ignore this message. /// Otherwise, set the Cancel property of the event argument to true. This will force a disconnection /// and trigger the ConnectedChanged event. /// /// public event System.ComponentModel.CancelEventHandler ConnectedToDefaultTarget; protected virtual void OnConnectedChanged() { ConnectedChanged?.Invoke(this, _plugin); } #endregion #region Helpers /// /// Receives data from the opened socket. /// The implementation of this function /// /// The destination buffer. /// The buffer size. /// The buffer offset. /// The number of bytes written in the buffer. /// 0 if operation succeeded, a non-zero value otherwise. /// Please use Windows Sockets Error Codes to return socket errors. protected virtual int Receive(byte[] buffer, int bufferSize, int bufferOffset, out int bytesReceived) { throw new NotImplementedException(); } /// /// Helper function handling socket listening, message rebuilding, and event sending once a message is received. /// The implementation of JD.Remoting.TCPConnection.Receive(byte[], int, int, out int) is required. /// /// 0 if operation succeeded, a non-zero value otherwise. /// See Windows Sockets Error Codes page for more information on error codes. protected int Receive() { int errorCode = Receive(m_receiveBuffer, BufferSize, 0, out int bytesReceived); if (errorCode == 0 && bytesReceived != 0) { m_messageBuilder.EnqueueRawData(m_receiveBuffer, bytesReceived); } while (m_messageBuilder.DequeueMessage(out Message message)) { HandleMessage(message); } Update?.Invoke(this, EventArgs.Empty); return errorCode; } /// /// Helper function allowing safe disposal of sockets. /// /// The socket to dispose. /// If true, the function will shutdown and disconnect properly the socket before attempting to /// close it. The shutdown is a blocking process, and will return once there is no pending data anymore on the socket. protected void DisposeSocket(ref Socket socket, bool cleanDisposal = true) { if (socket != null) { if (socket.Connected && cleanDisposal) { socket.Shutdown(SocketShutdown.Both); socket.Disconnect(false); } socket.Close(); socket = null; } } /// /// Helper function allowing safe disposal of sockets used in multithreaded environments. /// /// The socket to dispose. /// The mutex protecting the socket. /// If true, the function will shutdown and disconnect properly the socket before attempting to /// close it. The shutdown is a blocking process, and will return once there is no pending data anymore on the socket. protected void DisposeSocket(ref Socket socket, Mutex socketMutex, bool cleanDisposal = true) { using (new AutoMutex(socketMutex)) { DisposeSocket(ref socket, cleanDisposal); } } #endregion /// /// Gets a value that indicates whether the connection is established. /// public abstract bool IsConnected { get; } /// /// Tries to connect the socket. /// /// 0 if operation succeeded, a non-zero value otherwise. /// Please use Windows Sockets Error Codes to return socket errors. protected abstract int Connect(); /// /// Tries to disconnect the socket. /// /// 0 if operation succeeded, a non-zero value otherwise. /// Please use Windows Sockets Error Codes to return socket errors. protected abstract int Disconnect(); /// /// Sends a ping message to the engine, to ensure it is still alive. /// /// 0 if operation succeeded, a non-zero value otherwise. /// See Windows Sockets Error Codes page for more information on error codes. /// The System.Net.Socket has been closed. protected int SendPing() { return SendInternalCommand(CommandNames.Ping); } /// /// Sends an identification message to the engine, to link the tool to the associated plugin. /// /// 0 if operation succeeded, a non-zero value otherwise. /// See Windows Sockets Error Codes page for more information on error codes. /// The System.Net.Socket has been closed. protected int SendIdentify() { return SendInternalCommand(CommandNames.PluginId); } /// /// Sends a disconnection message to the engine, to signify the tool is aborting the connection. /// /// 0 if operation succeeded, a non-zero value otherwise. /// See Windows Sockets Error Codes page for more information on error codes. /// The System.Net.Socket has been closed. protected int SendDisconnect() { if (m_socket == null || !m_socket.Connected) return 0; // No need to disconnect the socket if it is already disconnected. return SendInternalCommand(CommandNames.Disconnect); } /// /// Internal commands use a wrong scheme (they inverse the command name and the plugin name ...) /// private int SendInternalCommand(string commandName) { return Send(new MessageHeader { PluginName = commandName, CommandName = PluginName }); } /// /// Called when the message is received, and ready to be sent to a handler. /// /// The message received. protected void HandleMessage(Message message) { NormalizeHeader(message); // Retrieve plugin name and command. if (m_pluginName != null && m_pluginName != UnknownPlugin && message.Header.PluginName != m_pluginName) throw new RemotingInvalidDataException("Message from " + message.Header.PluginName + " received, but the current connection can only handle messages from " + m_pluginName + " . "); if (message.Header.CommandName == CommandNames.PluginId) { IdentificationReceived?.Invoke(this, new MessageReceivedEventArgs(message)); } else if (message.Header.CommandName == CommandNames.Ping) { PingReceived?.Invoke(this, new MessageReceivedEventArgs(message)); } else if (message.Header.CommandName == CommandNames.Disconnect) { DisconnectionReceived?.Invoke(this, new MessageReceivedEventArgs(message)); } else if (message.Header.CommandName == CommandNames.ProjectId) { var e = new System.ComponentModel.CancelEventArgs(false); if (!CheckIfEngineIsCompatible(message.Content.ConvertToRead())) { ConnectedToIncompatibleTarget?.Invoke(this, e); if (e.Cancel) { Disconnect(); return; } } else { ConnectedToDefaultTarget?.Invoke(this, e); if (e.Cancel) { Disconnect(); return; } } // Dispatch the message to other handlers. Received?.Invoke(this, new MessageReceivedEventArgs(message)); } else { Received?.Invoke(this, new MessageReceivedEventArgs(message)); } } /// /// Some commands are sent in the plugin name, this normalizes the behavior. /// private void NormalizeHeader(Message message) { if (message.Header.PluginName == CommandNames.PluginId || message.Header.PluginName == CommandNames.Ping || message.Header.PluginName == CommandNames.Disconnect) { var newHeader = message.Header; newHeader.PluginName = message.Header.CommandName; newHeader.CommandName = message.Header.PluginName; message.Header = newHeader; } } /// /// Callback called by the connection thread. /// Use this callback to handle connection and data receive operations. /// protected abstract void ConnectionTaskCallback(); private void ConnectionTask() { while (!m_cancellationTokenSource.IsCancellationRequested) { if (m_autoConnect) { ConnectionTaskCallback(); } Thread.Sleep(1); } } private bool CheckIfEngineIsCompatible(IBlob blob) { ProjectId = blob.ExtractString(); return ProjectId == ProjectManager.Instance.GetCurrentProjectId(); } /// /// Occurs when a disconnection message is received. /// The command must handle the message contents immediately, as the message contents will be disposed /// just after the Received event was triggered. /// If you can't, create a blob clone with the JD.Remoting.Blob.Clone function and use this copy to complete your operations. /// public EventHandler DisconnectionReceived; /// /// Occurs when an identification message is received. /// The command must handle the message contents immediately, as the message contents will be disposed /// just after the Received event was triggered. /// If you can't, create a blob clone with the JD.Remoting.Blob.Clone function and use this copy to complete your operations. /// public EventHandler IdentificationReceived; /// /// Occurs when a ping message is received. /// The command must handle the message contents immediately, as the message contents will be disposed /// just after the Received event was triggered. /// If you can't, create a blob clone with the JD.Remoting.Blob.Clone function and use this copy to complete your operations. /// public EventHandler PingReceived; protected virtual void Dispose(bool disposing) { if (disposing) { m_cancellationTokenSource?.Cancel(); this.Disconnect(); DisposeSocket(ref m_socket, m_socketMutex, false); m_connectionTask?.Wait(); m_cancellationTokenSource?.Dispose(); m_cancellationTokenSource = null; m_connectionTask?.Dispose(); m_disposed = true; } } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } internal class NoPlugin : Plugin { /// public NoPlugin() : base("noplugin", null) { } } public class AutoMutex : IDisposable { private readonly Mutex m_mutex; public AutoMutex(Mutex mutex) { m_mutex = mutex; m_mutex.WaitOne(); } public void Dispose() { m_mutex.ReleaseMutex(); } } }