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();
}
}
}