123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- using System;
- using System.Diagnostics.CodeAnalysis;
- 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 DateTime? metaFileModifiedTime;
- private GodotIdeMetadata godotIdeMetadata;
- private readonly FileSystemWatcher fsWatcher;
- public string GodotEditorExecutablePath => godotIdeMetadata.EditorExecutablePath;
- 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
- [MemberNotNullWhen(true, "peer")]
- 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, ".godot", "mono", "metadata");
- // FileSystemWatcher requires an existing directory
- if (!Directory.Exists(projectMetadataDir))
- {
- // Check if the non hidden version exists
- string nonHiddenProjectMetadataDir = Path.Combine(godotProjectDir, "godot", "mono", "metadata");
- if (Directory.Exists(nonHiddenProjectMetadataDir))
- {
- projectMetadataDir = nonHiddenProjectMetadataDir;
- }
- else
- {
- Directory.CreateDirectory(projectMetadataDir);
- }
- }
- MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
- 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 lastWriteTime = File.GetLastWriteTime(MetaFilePath);
- if (lastWriteTime == metaFileModifiedTime)
- return;
- metaFileModifiedTime = lastWriteTime;
- 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 lastWriteTime = File.GetLastWriteTime(MetaFilePath);
- if (lastWriteTime == metaFileModifiedTime)
- return;
- metaFileModifiedTime = lastWriteTime;
- var metadata = ReadMetadataFile();
- if (metadata != null)
- {
- godotIdeMetadata = metadata.Value;
- _ = Task.Run(ConnectToServer);
- }
- }
- }
- private GodotIdeMetadata? ReadMetadataFile()
- {
- using (var fileStream = new FileStream(MetaFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
- using (var reader = new StreamReader(fileStream))
- {
- 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.Created += OnMetaFileChanged;
- 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);
- }
- }
- }
|