virtualx-engine/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs
Ignacio Etcheverry 0b94203a79 C#: Add Ide Connection library and server for the editor
This will be used for communicating between the Godot editor and external IDEs/editors, for things like opening files, triggering hot-reload and running the game with a debugger attached.
2019-08-04 01:57:53 +02:00

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