summaryrefslogtreecommitdiffstats
path: root/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs
diff options
context:
space:
mode:
Diffstat (limited to 'modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs')
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs332
1 files changed, 332 insertions, 0 deletions
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs
new file mode 100644
index 0000000000..d069651dd3
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs
@@ -0,0 +1,332 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using Newtonsoft.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging.Requests;
+using GodotTools.IdeMessaging.Utils;
+
+namespace GodotTools.IdeMessaging
+{
+ // ReSharper disable once UnusedType.Global
+ public sealed class Client : IDisposable
+ {
+ private readonly ILogger logger;
+
+ private readonly string identity;
+
+ private string MetaFilePath { get; }
+ private GodotIdeMetadata godotIdeMetadata;
+ private readonly FileSystemWatcher fsWatcher;
+
+ private readonly IMessageHandler messageHandler;
+
+ private Peer peer;
+ private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1);
+
+ private readonly Queue<NotifyAwaiter<bool>> clientConnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
+ private readonly Queue<NotifyAwaiter<bool>> clientDisconnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
+
+ // ReSharper disable once UnusedMember.Global
+ public async Task<bool> AwaitConnected()
+ {
+ var awaiter = new NotifyAwaiter<bool>();
+ clientConnectedAwaiters.Enqueue(awaiter);
+ return await awaiter;
+ }
+
+ // ReSharper disable once UnusedMember.Global
+ public async Task<bool> AwaitDisconnected()
+ {
+ var awaiter = new NotifyAwaiter<bool>();
+ clientDisconnectedAwaiters.Enqueue(awaiter);
+ return await awaiter;
+ }
+
+ // ReSharper disable once MemberCanBePrivate.Global
+ public bool IsDisposed { get; private set; }
+
+ // ReSharper disable once MemberCanBePrivate.Global
+ public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected;
+
+ // ReSharper disable once EventNeverSubscribedTo.Global
+ public event Action Connected
+ {
+ add
+ {
+ if (peer != null && !peer.IsDisposed)
+ peer.Connected += value;
+ }
+ remove
+ {
+ if (peer != null && !peer.IsDisposed)
+ peer.Connected -= value;
+ }
+ }
+
+ // ReSharper disable once EventNeverSubscribedTo.Global
+ public event Action Disconnected
+ {
+ add
+ {
+ if (peer != null && !peer.IsDisposed)
+ peer.Disconnected += value;
+ }
+ remove
+ {
+ if (peer != null && !peer.IsDisposed)
+ peer.Disconnected -= value;
+ }
+ }
+
+ ~Client()
+ {
+ Dispose(disposing: false);
+ }
+
+ public async void Dispose()
+ {
+ if (IsDisposed)
+ return;
+
+ using (await connectionSem.UseAsync())
+ {
+ if (IsDisposed) // lock may not be fair
+ return;
+ IsDisposed = true;
+ }
+
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ peer?.Dispose();
+ fsWatcher?.Dispose();
+ }
+ }
+
+ public Client(string identity, string godotProjectDir, IMessageHandler messageHandler, ILogger logger)
+ {
+ this.identity = identity;
+ this.messageHandler = messageHandler;
+ this.logger = logger;
+
+ string projectMetadataDir = Path.Combine(godotProjectDir, ".mono", "metadata");
+
+ MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
+
+ // FileSystemWatcher requires an existing directory
+ if (!File.Exists(projectMetadataDir))
+ Directory.CreateDirectory(projectMetadataDir);
+
+ fsWatcher = new FileSystemWatcher(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
+ }
+
+ private async void OnMetaFileChanged(object sender, FileSystemEventArgs e)
+ {
+ if (IsDisposed)
+ return;
+
+ using (await connectionSem.UseAsync())
+ {
+ if (IsDisposed)
+ return;
+
+ if (!File.Exists(MetaFilePath))
+ return;
+
+ var metadata = ReadMetadataFile();
+
+ if (metadata != null && metadata != godotIdeMetadata)
+ {
+ godotIdeMetadata = metadata.Value;
+ _ = Task.Run(ConnectToServer);
+ }
+ }
+ }
+
+ private async void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
+ {
+ if (IsDisposed)
+ return;
+
+ if (IsConnected)
+ {
+ using (await connectionSem.UseAsync())
+ peer?.Dispose();
+ }
+
+ // The file may have been re-created
+
+ using (await connectionSem.UseAsync())
+ {
+ if (IsDisposed)
+ return;
+
+ if (IsConnected || !File.Exists(MetaFilePath))
+ return;
+
+ var metadata = ReadMetadataFile();
+
+ if (metadata != null)
+ {
+ godotIdeMetadata = metadata.Value;
+ _ = Task.Run(ConnectToServer);
+ }
+ }
+ }
+
+ private GodotIdeMetadata? ReadMetadataFile()
+ {
+ using (var reader = File.OpenText(MetaFilePath))
+ {
+ string portStr = reader.ReadLine();
+
+ if (portStr == null)
+ return null;
+
+ string editorExecutablePath = reader.ReadLine();
+
+ if (editorExecutablePath == null)
+ return null;
+
+ if (!int.TryParse(portStr, out int port))
+ return null;
+
+ return new GodotIdeMetadata(port, editorExecutablePath);
+ }
+ }
+
+ private async Task AcceptClient(TcpClient tcpClient)
+ {
+ logger.LogDebug("Accept client...");
+
+ using (peer = new Peer(tcpClient, new ClientHandshake(), messageHandler, logger))
+ {
+ // ReSharper disable AccessToDisposedClosure
+ peer.Connected += () =>
+ {
+ logger.LogInfo("Connection open with Ide Client");
+
+ while (clientConnectedAwaiters.Count > 0)
+ clientConnectedAwaiters.Dequeue().SetResult(true);
+ };
+
+ peer.Disconnected += () =>
+ {
+ while (clientDisconnectedAwaiters.Count > 0)
+ clientDisconnectedAwaiters.Dequeue().SetResult(true);
+ };
+ // ReSharper restore AccessToDisposedClosure
+
+ try
+ {
+ if (!await peer.DoHandshake(identity))
+ {
+ logger.LogError("Handshake failed");
+ return;
+ }
+ }
+ catch (Exception e)
+ {
+ logger.LogError("Handshake failed with unhandled exception: ", e);
+ return;
+ }
+
+ await peer.Process();
+
+ logger.LogInfo("Connection closed with Ide Client");
+ }
+ }
+
+ private async Task ConnectToServer()
+ {
+ var tcpClient = new TcpClient();
+
+ try
+ {
+ logger.LogInfo("Connecting to Godot Ide Server");
+
+ await tcpClient.ConnectAsync(IPAddress.Loopback, godotIdeMetadata.Port);
+
+ logger.LogInfo("Connection open with Godot Ide Server");
+
+ await AcceptClient(tcpClient);
+ }
+ catch (SocketException e)
+ {
+ if (e.SocketErrorCode == SocketError.ConnectionRefused)
+ logger.LogError("The connection to the Godot Ide Server was refused");
+ else
+ throw;
+ }
+ }
+
+ // ReSharper disable once UnusedMember.Global
+ public async void Start()
+ {
+ fsWatcher.Changed += OnMetaFileChanged;
+ fsWatcher.Deleted += OnMetaFileDeleted;
+ fsWatcher.EnableRaisingEvents = true;
+
+ using (await connectionSem.UseAsync())
+ {
+ if (IsDisposed)
+ return;
+
+ if (IsConnected)
+ return;
+
+ if (!File.Exists(MetaFilePath))
+ {
+ logger.LogInfo("There is no Godot Ide Server running");
+ return;
+ }
+
+ var metadata = ReadMetadataFile();
+
+ if (metadata != null)
+ {
+ godotIdeMetadata = metadata.Value;
+ _ = Task.Run(ConnectToServer);
+ }
+ else
+ {
+ logger.LogError("Failed to read Godot Ide metadata file");
+ }
+ }
+ }
+
+ public async Task<TResponse> SendRequest<TResponse>(Request request)
+ where TResponse : Response, new()
+ {
+ if (!IsConnected)
+ {
+ logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
+ return null;
+ }
+
+ string body = JsonConvert.SerializeObject(request);
+ return await peer.SendRequest<TResponse>(request.Id, body);
+ }
+
+ public async Task<TResponse> SendRequest<TResponse>(string id, string body)
+ where TResponse : Response, new()
+ {
+ if (!IsConnected)
+ {
+ logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
+ return null;
+ }
+
+ return await peer.SendRequest<TResponse>(id, body);
+ }
+ }
+}