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