Client.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. using System;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Net;
  6. using System.Net.Sockets;
  7. using Newtonsoft.Json;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. using GodotTools.IdeMessaging.Requests;
  11. using GodotTools.IdeMessaging.Utils;
  12. namespace GodotTools.IdeMessaging
  13. {
  14. // ReSharper disable once UnusedType.Global
  15. public sealed class Client : IDisposable
  16. {
  17. private readonly ILogger logger;
  18. private readonly string identity;
  19. private string MetaFilePath { get; }
  20. private DateTime? metaFileModifiedTime;
  21. private GodotIdeMetadata godotIdeMetadata;
  22. private readonly FileSystemWatcher fsWatcher;
  23. public string GodotEditorExecutablePath => godotIdeMetadata.EditorExecutablePath;
  24. private readonly IMessageHandler messageHandler;
  25. private Peer? peer;
  26. private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1);
  27. private readonly Queue<NotifyAwaiter<bool>> clientConnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
  28. private readonly Queue<NotifyAwaiter<bool>> clientDisconnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
  29. // ReSharper disable once UnusedMember.Global
  30. public async Task<bool> AwaitConnected()
  31. {
  32. var awaiter = new NotifyAwaiter<bool>();
  33. clientConnectedAwaiters.Enqueue(awaiter);
  34. return await awaiter;
  35. }
  36. // ReSharper disable once UnusedMember.Global
  37. public async Task<bool> AwaitDisconnected()
  38. {
  39. var awaiter = new NotifyAwaiter<bool>();
  40. clientDisconnectedAwaiters.Enqueue(awaiter);
  41. return await awaiter;
  42. }
  43. // ReSharper disable once MemberCanBePrivate.Global
  44. public bool IsDisposed { get; private set; }
  45. // ReSharper disable once MemberCanBePrivate.Global
  46. [MemberNotNullWhen(true, "peer")]
  47. public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected;
  48. // ReSharper disable once EventNeverSubscribedTo.Global
  49. public event Action Connected
  50. {
  51. add
  52. {
  53. if (peer != null && !peer.IsDisposed)
  54. peer.Connected += value;
  55. }
  56. remove
  57. {
  58. if (peer != null && !peer.IsDisposed)
  59. peer.Connected -= value;
  60. }
  61. }
  62. // ReSharper disable once EventNeverSubscribedTo.Global
  63. public event Action Disconnected
  64. {
  65. add
  66. {
  67. if (peer != null && !peer.IsDisposed)
  68. peer.Disconnected += value;
  69. }
  70. remove
  71. {
  72. if (peer != null && !peer.IsDisposed)
  73. peer.Disconnected -= value;
  74. }
  75. }
  76. ~Client()
  77. {
  78. Dispose(disposing: false);
  79. }
  80. public async void Dispose()
  81. {
  82. if (IsDisposed)
  83. return;
  84. using (await connectionSem.UseAsync())
  85. {
  86. if (IsDisposed) // lock may not be fair
  87. return;
  88. IsDisposed = true;
  89. }
  90. Dispose(disposing: true);
  91. GC.SuppressFinalize(this);
  92. }
  93. private void Dispose(bool disposing)
  94. {
  95. if (disposing)
  96. {
  97. peer?.Dispose();
  98. fsWatcher.Dispose();
  99. }
  100. }
  101. public Client(string identity, string godotProjectDir, IMessageHandler messageHandler, ILogger logger)
  102. {
  103. this.identity = identity;
  104. this.messageHandler = messageHandler;
  105. this.logger = logger;
  106. string projectMetadataDir = Path.Combine(godotProjectDir, ".godot", "mono", "metadata");
  107. // FileSystemWatcher requires an existing directory
  108. if (!Directory.Exists(projectMetadataDir))
  109. {
  110. // Check if the non hidden version exists
  111. string nonHiddenProjectMetadataDir = Path.Combine(godotProjectDir, "godot", "mono", "metadata");
  112. if (Directory.Exists(nonHiddenProjectMetadataDir))
  113. {
  114. projectMetadataDir = nonHiddenProjectMetadataDir;
  115. }
  116. else
  117. {
  118. Directory.CreateDirectory(projectMetadataDir);
  119. }
  120. }
  121. MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
  122. fsWatcher = new FileSystemWatcher(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
  123. }
  124. private async void OnMetaFileChanged(object sender, FileSystemEventArgs e)
  125. {
  126. if (IsDisposed)
  127. return;
  128. using (await connectionSem.UseAsync())
  129. {
  130. if (IsDisposed)
  131. return;
  132. if (!File.Exists(MetaFilePath))
  133. return;
  134. var lastWriteTime = File.GetLastWriteTime(MetaFilePath);
  135. if (lastWriteTime == metaFileModifiedTime)
  136. return;
  137. metaFileModifiedTime = lastWriteTime;
  138. var metadata = ReadMetadataFile();
  139. if (metadata != null && metadata != godotIdeMetadata)
  140. {
  141. godotIdeMetadata = metadata.Value;
  142. _ = Task.Run(ConnectToServer);
  143. }
  144. }
  145. }
  146. private async void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
  147. {
  148. if (IsDisposed)
  149. return;
  150. if (IsConnected)
  151. {
  152. using (await connectionSem.UseAsync())
  153. peer?.Dispose();
  154. }
  155. // The file may have been re-created
  156. using (await connectionSem.UseAsync())
  157. {
  158. if (IsDisposed)
  159. return;
  160. if (IsConnected || !File.Exists(MetaFilePath))
  161. return;
  162. var lastWriteTime = File.GetLastWriteTime(MetaFilePath);
  163. if (lastWriteTime == metaFileModifiedTime)
  164. return;
  165. metaFileModifiedTime = lastWriteTime;
  166. var metadata = ReadMetadataFile();
  167. if (metadata != null)
  168. {
  169. godotIdeMetadata = metadata.Value;
  170. _ = Task.Run(ConnectToServer);
  171. }
  172. }
  173. }
  174. private GodotIdeMetadata? ReadMetadataFile()
  175. {
  176. using (var fileStream = new FileStream(MetaFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
  177. using (var reader = new StreamReader(fileStream))
  178. {
  179. string? portStr = reader.ReadLine();
  180. if (portStr == null)
  181. return null;
  182. string? editorExecutablePath = reader.ReadLine();
  183. if (editorExecutablePath == null)
  184. return null;
  185. if (!int.TryParse(portStr, out int port))
  186. return null;
  187. return new GodotIdeMetadata(port, editorExecutablePath);
  188. }
  189. }
  190. private async Task AcceptClient(TcpClient tcpClient)
  191. {
  192. logger.LogDebug("Accept client...");
  193. using (peer = new Peer(tcpClient, new ClientHandshake(), messageHandler, logger))
  194. {
  195. // ReSharper disable AccessToDisposedClosure
  196. peer.Connected += () =>
  197. {
  198. logger.LogInfo("Connection open with Ide Client");
  199. while (clientConnectedAwaiters.Count > 0)
  200. clientConnectedAwaiters.Dequeue().SetResult(true);
  201. };
  202. peer.Disconnected += () =>
  203. {
  204. while (clientDisconnectedAwaiters.Count > 0)
  205. clientDisconnectedAwaiters.Dequeue().SetResult(true);
  206. };
  207. // ReSharper restore AccessToDisposedClosure
  208. try
  209. {
  210. if (!await peer.DoHandshake(identity))
  211. {
  212. logger.LogError("Handshake failed");
  213. return;
  214. }
  215. }
  216. catch (Exception e)
  217. {
  218. logger.LogError("Handshake failed with unhandled exception: ", e);
  219. return;
  220. }
  221. await peer.Process();
  222. logger.LogInfo("Connection closed with Ide Client");
  223. }
  224. }
  225. private async Task ConnectToServer()
  226. {
  227. var tcpClient = new TcpClient();
  228. try
  229. {
  230. logger.LogInfo("Connecting to Godot Ide Server");
  231. await tcpClient.ConnectAsync(IPAddress.Loopback, godotIdeMetadata.Port);
  232. logger.LogInfo("Connection open with Godot Ide Server");
  233. await AcceptClient(tcpClient);
  234. }
  235. catch (SocketException e)
  236. {
  237. if (e.SocketErrorCode == SocketError.ConnectionRefused)
  238. logger.LogError("The connection to the Godot Ide Server was refused");
  239. else
  240. throw;
  241. }
  242. }
  243. // ReSharper disable once UnusedMember.Global
  244. public async void Start()
  245. {
  246. fsWatcher.Created += OnMetaFileChanged;
  247. fsWatcher.Changed += OnMetaFileChanged;
  248. fsWatcher.Deleted += OnMetaFileDeleted;
  249. fsWatcher.EnableRaisingEvents = true;
  250. using (await connectionSem.UseAsync())
  251. {
  252. if (IsDisposed)
  253. return;
  254. if (IsConnected)
  255. return;
  256. if (!File.Exists(MetaFilePath))
  257. {
  258. logger.LogInfo("There is no Godot Ide Server running");
  259. return;
  260. }
  261. var metadata = ReadMetadataFile();
  262. if (metadata != null)
  263. {
  264. godotIdeMetadata = metadata.Value;
  265. _ = Task.Run(ConnectToServer);
  266. }
  267. else
  268. {
  269. logger.LogError("Failed to read Godot Ide metadata file");
  270. }
  271. }
  272. }
  273. public async Task<TResponse?> SendRequest<TResponse>(Request request)
  274. where TResponse : Response, new()
  275. {
  276. if (!IsConnected)
  277. {
  278. logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
  279. return null;
  280. }
  281. string body = JsonConvert.SerializeObject(request);
  282. return await peer.SendRequest<TResponse>(request.Id, body);
  283. }
  284. public async Task<TResponse?> SendRequest<TResponse>(string id, string body)
  285. where TResponse : Response, new()
  286. {
  287. if (!IsConnected)
  288. {
  289. logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
  290. return null;
  291. }
  292. return await peer.SendRequest<TResponse>(id, body);
  293. }
  294. }
  295. }