508 lines
20 KiB
C#
508 lines
20 KiB
C#
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";
|
|
|
|
/// <summary>
|
|
/// Gets the size of the buffer used by the JD.Remoting.TCPConnection.Receive() helper function.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Default constructor.
|
|
/// </summary>
|
|
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; }
|
|
|
|
/// <summary>
|
|
/// Opens a TCP connection.
|
|
/// </summary>
|
|
/// <returns>Zero if succeeded, a non-zero value otherwise. </returns>
|
|
public virtual int Open(IEndPointId endPointId)
|
|
{
|
|
m_autoConnect = true;
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the connection.
|
|
/// </summary>
|
|
/// <returns>Always zero. </returns>
|
|
public virtual int Close()
|
|
{
|
|
m_autoConnect = false;
|
|
Disconnect();
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a message to the host connected.
|
|
/// </summary>
|
|
/// <param name="message">A message to send. </param>
|
|
/// <returns>0 if operation succeeded, a non-zero value otherwise.
|
|
/// See Windows Sockets Error Codes page for more information on error codes. </returns>
|
|
/// <exception cref="System.ObjectDisposedException">The System.Net.Socket has been closed. </exception>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs regularly. Meant to be used to send some messages asynchronously, but can be used as you wish.
|
|
/// <remarks>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.
|
|
/// </remarks>
|
|
/// </summary>
|
|
public event EventHandler Update;
|
|
|
|
/// <summary>
|
|
/// Occurs when a message is received.
|
|
/// <remarks>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. </remarks>
|
|
/// </summary>
|
|
/// <remarks>This event may not be called if a binding command is set.</remarks>
|
|
public event EventHandler<IMessageReceivedEventArgs> Received;
|
|
|
|
/// <summary>
|
|
/// Triggered when the connection state changes.
|
|
/// </summary>
|
|
public event EventHandler<IPlugin> ConnectedChanged;
|
|
|
|
/// <summary>
|
|
/// Triggered when the connection target is not compatible with the current connection.
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
/// </summary>
|
|
public event System.ComponentModel.CancelEventHandler ConnectedToIncompatibleTarget;
|
|
|
|
/// <summary>
|
|
/// Triggered when the connection target could not find a valid engine branch specification, but did use
|
|
/// the default specification.
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
/// </summary>
|
|
public event System.ComponentModel.CancelEventHandler ConnectedToDefaultTarget;
|
|
|
|
protected virtual void OnConnectedChanged()
|
|
{
|
|
ConnectedChanged?.Invoke(this, _plugin);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
/// <summary>
|
|
/// Receives data from the opened socket.
|
|
/// The implementation of this function
|
|
/// </summary>
|
|
/// <param name="buffer">The destination buffer. </param>
|
|
/// <param name="bufferSize">The buffer size. </param>
|
|
/// <param name="bufferOffset">The buffer offset. </param>
|
|
/// <param name="bytesReceived">The number of bytes written in the buffer. </param>
|
|
/// <returns>0 if operation succeeded, a non-zero value otherwise.
|
|
/// Please use Windows Sockets Error Codes to return socket errors. </returns>
|
|
protected virtual int Receive(byte[] buffer, int bufferSize, int bufferOffset, out int bytesReceived)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <returns>0 if operation succeeded, a non-zero value otherwise.
|
|
/// See Windows Sockets Error Codes page for more information on error codes. </returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function allowing safe disposal of sockets.
|
|
/// </summary>
|
|
/// <param name="socket">The socket to dispose. </param>
|
|
/// <param name="cleanDisposal">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. </param>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function allowing safe disposal of sockets used in multithreaded environments.
|
|
/// </summary>
|
|
/// <param name="socket">The socket to dispose. </param>
|
|
/// <param name="socketMutex">The mutex protecting the socket. </param>
|
|
/// <param name="cleanDisposal">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. </param>
|
|
protected void DisposeSocket(ref Socket socket, Mutex socketMutex, bool cleanDisposal = true)
|
|
{
|
|
using (new AutoMutex(socketMutex))
|
|
{
|
|
DisposeSocket(ref socket, cleanDisposal);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Gets a value that indicates whether the connection is established.
|
|
/// </summary>
|
|
public abstract bool IsConnected { get; }
|
|
|
|
/// <summary>
|
|
/// Tries to connect the socket.
|
|
/// </summary>
|
|
/// <returns>0 if operation succeeded, a non-zero value otherwise.
|
|
/// Please use Windows Sockets Error Codes to return socket errors. </returns>
|
|
protected abstract int Connect();
|
|
|
|
/// <summary>
|
|
/// Tries to disconnect the socket.
|
|
/// </summary>
|
|
/// <returns>0 if operation succeeded, a non-zero value otherwise.
|
|
/// Please use Windows Sockets Error Codes to return socket errors. </returns>
|
|
protected abstract int Disconnect();
|
|
|
|
/// <summary>
|
|
/// Sends a ping message to the engine, to ensure it is still alive.
|
|
/// </summary>
|
|
/// <returns>0 if operation succeeded, a non-zero value otherwise.
|
|
/// See Windows Sockets Error Codes page for more information on error codes. </returns>
|
|
/// <exception cref="System.ObjectDisposedException">The System.Net.Socket has been closed. </exception>
|
|
protected int SendPing()
|
|
{
|
|
return SendInternalCommand(CommandNames.Ping);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends an identification message to the engine, to link the tool to the associated plugin.
|
|
/// </summary>
|
|
/// <returns>0 if operation succeeded, a non-zero value otherwise.
|
|
/// See Windows Sockets Error Codes page for more information on error codes. </returns>
|
|
/// <exception cref="System.ObjectDisposedException">The System.Net.Socket has been closed. </exception>
|
|
protected int SendIdentify()
|
|
{
|
|
return SendInternalCommand(CommandNames.PluginId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a disconnection message to the engine, to signify the tool is aborting the connection.
|
|
/// </summary>
|
|
/// <returns>0 if operation succeeded, a non-zero value otherwise.
|
|
/// See Windows Sockets Error Codes page for more information on error codes. </returns>
|
|
/// <exception cref="System.ObjectDisposedException">The System.Net.Socket has been closed. </exception>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal commands use a wrong scheme (they inverse the command name and the plugin name ...)
|
|
/// </summary>
|
|
private int SendInternalCommand(string commandName)
|
|
{
|
|
return Send(new MessageHeader { PluginName = commandName, CommandName = PluginName });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the message is received, and ready to be sent to a handler.
|
|
/// </summary>
|
|
/// <param name="message">The message received. </param>
|
|
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));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Some commands are sent in the plugin name, this normalizes the behavior.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback called by the connection thread.
|
|
/// Use this callback to handle connection and data receive operations.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when a disconnection message is received.
|
|
/// <remarks>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. </remarks>
|
|
/// </summary>
|
|
public EventHandler<MessageReceivedEventArgs> DisconnectionReceived;
|
|
|
|
/// <summary>
|
|
/// Occurs when an identification message is received.
|
|
/// <remarks>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. </remarks>
|
|
/// </summary>
|
|
public EventHandler<MessageReceivedEventArgs> IdentificationReceived;
|
|
|
|
/// <summary>
|
|
/// Occurs when a ping message is received.
|
|
/// <remarks>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. </remarks>
|
|
/// </summary>
|
|
public EventHandler<MessageReceivedEventArgs> 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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|
|
|
|
internal class NoPlugin : Plugin
|
|
{
|
|
/// <inheritdoc />
|
|
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();
|
|
}
|
|
}
|
|
}
|