208 lines
6.1 KiB
C#
208 lines
6.1 KiB
C#
|
using System;
|
||
|
using System.Diagnostics;
|
||
|
using System.IO;
|
||
|
using System.Net.Sockets;
|
||
|
using System.Text;
|
||
|
|
||
|
namespace GodotTools.IdeConnection
|
||
|
{
|
||
|
public abstract class GodotIdeConnection : IDisposable
|
||
|
{
|
||
|
protected const string Version = "1.0";
|
||
|
|
||
|
protected static readonly string ClientHandshake = $"Godot Ide Client Version {Version}";
|
||
|
protected static readonly string ServerHandshake = $"Godot Ide Server Version {Version}";
|
||
|
|
||
|
private const int ClientWriteTimeout = 8000;
|
||
|
private readonly TcpClient tcpClient;
|
||
|
|
||
|
private TextReader clientReader;
|
||
|
private TextWriter clientWriter;
|
||
|
|
||
|
private readonly object writeLock = new object();
|
||
|
|
||
|
private readonly Func<Message, bool> messageHandler;
|
||
|
|
||
|
public event Action Connected;
|
||
|
|
||
|
private ILogger logger;
|
||
|
|
||
|
public ILogger Logger
|
||
|
{
|
||
|
get => logger ?? (logger = new ConsoleLogger());
|
||
|
set => logger = value;
|
||
|
}
|
||
|
|
||
|
public bool IsDisposed { get; private set; } = false;
|
||
|
|
||
|
public bool IsConnected => tcpClient.Client != null && tcpClient.Client.Connected;
|
||
|
|
||
|
protected GodotIdeConnection(TcpClient tcpClient, Func<Message, bool> messageHandler)
|
||
|
{
|
||
|
this.tcpClient = tcpClient;
|
||
|
this.messageHandler = messageHandler;
|
||
|
}
|
||
|
|
||
|
public void Start()
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
if (!StartConnection())
|
||
|
return;
|
||
|
|
||
|
string messageLine;
|
||
|
while ((messageLine = ReadLine()) != null)
|
||
|
{
|
||
|
if (!MessageParser.TryParse(messageLine, out Message msg))
|
||
|
{
|
||
|
Logger.LogError($"Received message with invalid format: {messageLine}");
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
Logger.LogDebug($"Received message: {msg}");
|
||
|
|
||
|
if (msg.Id == "close")
|
||
|
{
|
||
|
Logger.LogInfo("Closing connection");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
Debug.Assert(messageHandler != null);
|
||
|
|
||
|
if (!messageHandler(msg))
|
||
|
Logger.LogError($"Received unknown message: {msg}");
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Logger.LogError($"Message handler for '{msg}' failed with exception", e);
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Logger.LogError($"Exception thrown from message handler. Message: {msg}", e);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
Logger.LogError($"Unhandled exception in the Godot Ide Connection thread", e);
|
||
|
}
|
||
|
finally
|
||
|
{
|
||
|
Dispose();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private bool StartConnection()
|
||
|
{
|
||
|
NetworkStream clientStream = tcpClient.GetStream();
|
||
|
|
||
|
clientReader = new StreamReader(clientStream, Encoding.UTF8);
|
||
|
|
||
|
lock (writeLock)
|
||
|
clientWriter = new StreamWriter(clientStream, Encoding.UTF8);
|
||
|
|
||
|
clientStream.WriteTimeout = ClientWriteTimeout;
|
||
|
|
||
|
if (!WriteHandshake())
|
||
|
{
|
||
|
Logger.LogError("Could not write handshake");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!IsValidResponseHandshake(ReadLine()))
|
||
|
{
|
||
|
Logger.LogError("Received invalid handshake");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
Connected?.Invoke();
|
||
|
|
||
|
Logger.LogInfo("Godot Ide connection started");
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private string ReadLine()
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
return clientReader?.ReadLine();
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
if (IsDisposed)
|
||
|
{
|
||
|
var se = e as SocketException ?? e.InnerException as SocketException;
|
||
|
if (se != null && se.SocketErrorCode == SocketError.Interrupted)
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
throw;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public bool WriteMessage(Message message)
|
||
|
{
|
||
|
Logger.LogDebug($"Sending message {message}");
|
||
|
|
||
|
var messageComposer = new MessageComposer();
|
||
|
|
||
|
messageComposer.AddArgument(message.Id);
|
||
|
foreach (string argument in message.Arguments)
|
||
|
messageComposer.AddArgument(argument);
|
||
|
|
||
|
return WriteLine(messageComposer.ToString());
|
||
|
}
|
||
|
|
||
|
protected bool WriteLine(string text)
|
||
|
{
|
||
|
if (clientWriter == null || IsDisposed || !IsConnected)
|
||
|
return false;
|
||
|
|
||
|
lock (writeLock)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
clientWriter.WriteLine(text);
|
||
|
clientWriter.Flush();
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
if (!IsDisposed)
|
||
|
{
|
||
|
var se = e as SocketException ?? e.InnerException as SocketException;
|
||
|
if (se != null && se.SocketErrorCode == SocketError.Shutdown)
|
||
|
Logger.LogInfo("Client disconnected ungracefully");
|
||
|
else
|
||
|
Logger.LogError("Exception thrown when trying to write to client", e);
|
||
|
|
||
|
Dispose();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
protected abstract bool WriteHandshake();
|
||
|
protected abstract bool IsValidResponseHandshake(string handshakeLine);
|
||
|
|
||
|
public void Dispose()
|
||
|
{
|
||
|
if (IsDisposed)
|
||
|
return;
|
||
|
|
||
|
IsDisposed = true;
|
||
|
|
||
|
clientReader?.Dispose();
|
||
|
clientWriter?.Dispose();
|
||
|
((IDisposable) tcpClient)?.Dispose();
|
||
|
}
|
||
|
}
|
||
|
}
|