initial commit, 4.5 stable
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
This commit is contained in:
363
modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs
Normal file
363
modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs
Normal file
@@ -0,0 +1,363 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public class ClientHandshake : IHandshake
|
||||
{
|
||||
private static readonly string ClientHandshakeBase = $"{Peer.ClientHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}";
|
||||
private static readonly string ServerHandshakePattern = $@"{Regex.Escape(Peer.ServerHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})";
|
||||
|
||||
public string GetHandshakeLine(string identity) => $"{ClientHandshakeBase},{identity}";
|
||||
|
||||
public bool IsValidPeerHandshake(string handshake, [NotNullWhen(true)] out string? identity, ILogger logger)
|
||||
{
|
||||
identity = null;
|
||||
|
||||
var match = Regex.Match(handshake, ServerHandshakePattern);
|
||||
|
||||
if (!match.Success)
|
||||
return false;
|
||||
|
||||
if (!uint.TryParse(match.Groups[1].Value, out uint serverMajor) || Peer.ProtocolVersionMajor != serverMajor)
|
||||
{
|
||||
logger.LogDebug("Incompatible major version: " + match.Groups[1].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(match.Groups[2].Value, out uint serverMinor) || Peer.ProtocolVersionMinor < serverMinor)
|
||||
{
|
||||
logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision
|
||||
{
|
||||
logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
identity = match.Groups[4].Value;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
// ReSharper disable once UnusedType.Global
|
||||
public abstract class ClientMessageHandler : IMessageHandler
|
||||
{
|
||||
private readonly Dictionary<string, Peer.RequestHandler> requestHandlers;
|
||||
|
||||
protected ClientMessageHandler()
|
||||
{
|
||||
requestHandlers = InitializeRequestHandlers();
|
||||
}
|
||||
|
||||
public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
|
||||
{
|
||||
if (!requestHandlers.TryGetValue(id, out var handler))
|
||||
{
|
||||
logger.LogError($"Received unknown request: {id}");
|
||||
return new MessageContent(MessageStatus.RequestNotSupported, "null");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await handler(peer, content);
|
||||
return new MessageContent(response.Status, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
logger.LogError($"Received request with invalid body: {id}");
|
||||
return new MessageContent(MessageStatus.InvalidRequestBody, "null");
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, Peer.RequestHandler> InitializeRequestHandlers()
|
||||
{
|
||||
return new Dictionary<string, Peer.RequestHandler>
|
||||
{
|
||||
[OpenFileRequest.Id] = async (peer, content) =>
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<OpenFileRequest>(content.Body);
|
||||
return await HandleOpenFile(request!);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract Task<Response> HandleOpenFile(OpenFileRequest request);
|
||||
}
|
||||
}
|
@@ -0,0 +1,142 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
|
||||
internal sealed class AllowNullAttribute : Attribute
|
||||
{ }
|
||||
|
||||
/// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
|
||||
internal sealed class DisallowNullAttribute : Attribute
|
||||
{ }
|
||||
|
||||
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
|
||||
internal sealed class MaybeNullAttribute : Attribute
|
||||
{ }
|
||||
|
||||
/// <summary>Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
|
||||
internal sealed class NotNullAttribute : Attribute
|
||||
{ }
|
||||
|
||||
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter may be null even if the corresponding type disallows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
internal sealed class MaybeNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter may be null.
|
||||
/// </param>
|
||||
public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
|
||||
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
internal sealed class NotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
|
||||
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class NotNullIfNotNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the associated parameter name.</summary>
|
||||
/// <param name="parameterName">
|
||||
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
|
||||
/// </param>
|
||||
public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName;
|
||||
|
||||
/// <summary>Gets the associated parameter name.</summary>
|
||||
public string ParameterName { get; }
|
||||
}
|
||||
|
||||
/// <summary>Applied to a method that will never return under any circumstance.</summary>
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
internal sealed class DoesNotReturnAttribute : Attribute
|
||||
{ }
|
||||
|
||||
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
internal sealed class DoesNotReturnIfAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified parameter value.</summary>
|
||||
/// <param name="parameterValue">
|
||||
/// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
|
||||
/// the associated parameter matches this value.
|
||||
/// </param>
|
||||
public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue;
|
||||
|
||||
/// <summary>Gets the condition parameter value.</summary>
|
||||
public bool ParameterValue { get; }
|
||||
}
|
||||
|
||||
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values.</summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
|
||||
internal sealed class MemberNotNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with a field or property member.</summary>
|
||||
/// <param name="member">
|
||||
/// The field or property member that is promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullAttribute(string member) => Members = new[] { member };
|
||||
|
||||
/// <summary>Initializes the attribute with the list of field and property members.</summary>
|
||||
/// <param name="members">
|
||||
/// The list of field and property members that are promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullAttribute(params string[] members) => Members = members;
|
||||
|
||||
/// <summary>Gets field or property member names.</summary>
|
||||
public string[] Members { get; }
|
||||
}
|
||||
|
||||
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition.</summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
|
||||
internal sealed class MemberNotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
/// <param name="member">
|
||||
/// The field or property member that is promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullWhenAttribute(bool returnValue, string member)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = new[] { member };
|
||||
}
|
||||
|
||||
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
/// <param name="members">
|
||||
/// The list of field and property members that are promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = members;
|
||||
}
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
|
||||
/// <summary>Gets field or property member names.</summary>
|
||||
public string[] Members { get; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public readonly struct GodotIdeMetadata
|
||||
{
|
||||
public int Port { get; }
|
||||
public string EditorExecutablePath { get; }
|
||||
|
||||
public const string DefaultFileName = "ide_messaging_meta.txt";
|
||||
|
||||
public GodotIdeMetadata(int port, string editorExecutablePath)
|
||||
{
|
||||
Port = port;
|
||||
EditorExecutablePath = editorExecutablePath;
|
||||
}
|
||||
|
||||
public static bool operator ==(GodotIdeMetadata a, GodotIdeMetadata b)
|
||||
{
|
||||
return a.Port == b.Port && a.EditorExecutablePath == b.EditorExecutablePath;
|
||||
}
|
||||
|
||||
public static bool operator !=(GodotIdeMetadata a, GodotIdeMetadata b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is GodotIdeMetadata metadata && metadata == this;
|
||||
}
|
||||
|
||||
public bool Equals(GodotIdeMetadata other)
|
||||
{
|
||||
return Port == other.Port && EditorExecutablePath == other.EditorExecutablePath;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (Port * 397) ^ (EditorExecutablePath != null ? EditorExecutablePath.GetHashCode() : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>9</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageId>GodotTools.IdeMessaging</PackageId>
|
||||
<Version>1.1.2</Version>
|
||||
<AssemblyVersion>$(Version)</AssemblyVersion>
|
||||
<Authors>Godot Engine contributors</Authors>
|
||||
<Company />
|
||||
<PackageTags>godot</PackageTags>
|
||||
<RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/GodotTools/GodotTools.IdeMessaging</RepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Copyright>Copyright (c) Godot Engine contributors</Copyright>
|
||||
<Description>
|
||||
This library enables communication with the Godot Engine editor (the version with .NET support).
|
||||
It's intended for use in IDEs/editors plugins for a better experience working with Godot C# projects.
|
||||
|
||||
A client using this library is only compatible with servers of the same major version and of a lower or equal minor version.
|
||||
</Description>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@@ -0,0 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public interface IHandshake
|
||||
{
|
||||
string GetHandshakeLine(string identity);
|
||||
bool IsValidPeerHandshake(string handshake, [NotNullWhen(true)] out string? identity, ILogger logger);
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void LogDebug(string message);
|
||||
void LogInfo(string message);
|
||||
void LogWarning(string message);
|
||||
void LogError(string message);
|
||||
void LogError(string message, Exception e);
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public interface IMessageHandler
|
||||
{
|
||||
Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger);
|
||||
}
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public class Message
|
||||
{
|
||||
public MessageKind Kind { get; }
|
||||
public string Id { get; }
|
||||
public MessageContent Content { get; }
|
||||
|
||||
public Message(MessageKind kind, string id, MessageContent content)
|
||||
{
|
||||
Kind = kind;
|
||||
Id = id;
|
||||
Content = content;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Kind} | {Id}";
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageKind
|
||||
{
|
||||
Request,
|
||||
Response
|
||||
}
|
||||
|
||||
public enum MessageStatus
|
||||
{
|
||||
Ok,
|
||||
RequestNotSupported,
|
||||
InvalidRequestBody
|
||||
}
|
||||
|
||||
public readonly struct MessageContent
|
||||
{
|
||||
public MessageStatus Status { get; }
|
||||
public string Body { get; }
|
||||
|
||||
public MessageContent(string body)
|
||||
{
|
||||
Status = MessageStatus.Ok;
|
||||
Body = body;
|
||||
}
|
||||
|
||||
public MessageContent(MessageStatus status, string body)
|
||||
{
|
||||
Status = status;
|
||||
Body = body;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public class MessageDecoder
|
||||
{
|
||||
private class DecodedMessage
|
||||
{
|
||||
public MessageKind? Kind;
|
||||
public string? Id;
|
||||
public MessageStatus? Status;
|
||||
public readonly StringBuilder Body = new StringBuilder();
|
||||
public uint? PendingBodyLines;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Kind = null;
|
||||
Id = null;
|
||||
Status = null;
|
||||
Body.Clear();
|
||||
PendingBodyLines = null;
|
||||
}
|
||||
|
||||
public Message ToMessage()
|
||||
{
|
||||
if (!Kind.HasValue || Id == null || !Status.HasValue ||
|
||||
!PendingBodyLines.HasValue || PendingBodyLines.Value > 0)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
return new Message(Kind.Value, Id, new MessageContent(Status.Value, Body.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
public enum State
|
||||
{
|
||||
Decoding,
|
||||
Decoded,
|
||||
Errored
|
||||
}
|
||||
|
||||
private readonly DecodedMessage decodingMessage = new DecodedMessage();
|
||||
|
||||
public State Decode(string messageLine, out Message? decodedMessage)
|
||||
{
|
||||
decodedMessage = null;
|
||||
|
||||
if (!decodingMessage.Kind.HasValue)
|
||||
{
|
||||
if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageKind kind))
|
||||
{
|
||||
decodingMessage.Clear();
|
||||
return State.Errored;
|
||||
}
|
||||
|
||||
decodingMessage.Kind = kind;
|
||||
}
|
||||
else if (decodingMessage.Id == null)
|
||||
{
|
||||
decodingMessage.Id = messageLine;
|
||||
}
|
||||
else if (decodingMessage.Status == null)
|
||||
{
|
||||
if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageStatus status))
|
||||
{
|
||||
decodingMessage.Clear();
|
||||
return State.Errored;
|
||||
}
|
||||
|
||||
decodingMessage.Status = status;
|
||||
}
|
||||
else if (decodingMessage.PendingBodyLines == null)
|
||||
{
|
||||
if (!uint.TryParse(messageLine, out uint pendingBodyLines))
|
||||
{
|
||||
decodingMessage.Clear();
|
||||
return State.Errored;
|
||||
}
|
||||
|
||||
decodingMessage.PendingBodyLines = pendingBodyLines;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (decodingMessage.PendingBodyLines > 0)
|
||||
{
|
||||
decodingMessage.Body.AppendLine(messageLine);
|
||||
decodingMessage.PendingBodyLines -= 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
decodedMessage = decodingMessage.ToMessage();
|
||||
decodingMessage.Clear();
|
||||
return State.Decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return State.Decoding;
|
||||
}
|
||||
}
|
||||
}
|
298
modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs
Normal file
298
modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using GodotTools.IdeMessaging.Utils;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public sealed class Peer : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Major version.
|
||||
/// There is no forward nor backward compatibility between different major versions.
|
||||
/// Connection is refused if client and server have different major versions.
|
||||
/// </summary>
|
||||
public static readonly int ProtocolVersionMajor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Major;
|
||||
|
||||
/// <summary>
|
||||
/// Minor version, which clients must be backward compatible with.
|
||||
/// Connection is refused if the client's minor version is lower than the server's.
|
||||
/// </summary>
|
||||
public static readonly int ProtocolVersionMinor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Minor;
|
||||
|
||||
/// <summary>
|
||||
/// Revision, which doesn't affect compatibility.
|
||||
/// </summary>
|
||||
public static readonly int ProtocolVersionRevision = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Revision;
|
||||
|
||||
public const string ClientHandshakeName = "GodotIdeClient";
|
||||
public const string ServerHandshakeName = "GodotIdeServer";
|
||||
|
||||
private const int ClientWriteTimeout = 8000;
|
||||
|
||||
public delegate Task<Response> RequestHandler(Peer peer, MessageContent content);
|
||||
|
||||
private readonly TcpClient tcpClient;
|
||||
|
||||
private readonly TextReader clientReader;
|
||||
private readonly TextWriter clientWriter;
|
||||
|
||||
private readonly SemaphoreSlim writeSem = new SemaphoreSlim(1);
|
||||
|
||||
private string? remoteIdentity;
|
||||
public string RemoteIdentity => remoteIdentity ??= string.Empty;
|
||||
|
||||
public event Action? Connected;
|
||||
public event Action? Disconnected;
|
||||
|
||||
private ILogger Logger { get; }
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public bool IsTcpClientConnected => tcpClient.Client != null && tcpClient.Client.Connected;
|
||||
|
||||
private bool IsConnected { get; set; }
|
||||
|
||||
private readonly IHandshake handshake;
|
||||
private readonly IMessageHandler messageHandler;
|
||||
|
||||
private readonly Dictionary<string, Queue<ResponseAwaiter>> requestAwaiterQueues = new Dictionary<string, Queue<ResponseAwaiter>>();
|
||||
private readonly SemaphoreSlim requestsSem = new SemaphoreSlim(1);
|
||||
|
||||
public Peer(TcpClient tcpClient, IHandshake handshake, IMessageHandler messageHandler, ILogger logger)
|
||||
{
|
||||
this.tcpClient = tcpClient;
|
||||
this.handshake = handshake;
|
||||
this.messageHandler = messageHandler;
|
||||
|
||||
Logger = logger;
|
||||
|
||||
NetworkStream clientStream = tcpClient.GetStream();
|
||||
clientStream.WriteTimeout = ClientWriteTimeout;
|
||||
|
||||
clientReader = new StreamReader(clientStream, Encoding.UTF8);
|
||||
clientWriter = new StreamWriter(clientStream, Encoding.UTF8) { NewLine = "\n" };
|
||||
}
|
||||
|
||||
public async Task Process()
|
||||
{
|
||||
try
|
||||
{
|
||||
var decoder = new MessageDecoder();
|
||||
|
||||
string? messageLine;
|
||||
while ((messageLine = await ReadLine()) != null)
|
||||
{
|
||||
var state = decoder.Decode(messageLine, out var msg);
|
||||
|
||||
if (state == MessageDecoder.State.Decoding)
|
||||
continue; // Not finished decoding yet
|
||||
|
||||
if (state == MessageDecoder.State.Errored)
|
||||
{
|
||||
Logger.LogError($"Received message line with invalid format: {messageLine}");
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Received message: {msg}");
|
||||
|
||||
try
|
||||
{
|
||||
if (msg!.Kind == MessageKind.Request)
|
||||
{
|
||||
var responseContent = await messageHandler.HandleRequest(this, msg.Id, msg.Content, Logger);
|
||||
await WriteMessage(new Message(MessageKind.Response, msg.Id, responseContent));
|
||||
}
|
||||
else if (msg.Kind == MessageKind.Response)
|
||||
{
|
||||
ResponseAwaiter responseAwaiter;
|
||||
|
||||
using (await requestsSem.UseAsync())
|
||||
{
|
||||
if (!requestAwaiterQueues.TryGetValue(msg.Id, out var queue) || queue.Count <= 0)
|
||||
{
|
||||
Logger.LogError($"Received unexpected response: {msg.Id}");
|
||||
return;
|
||||
}
|
||||
|
||||
responseAwaiter = queue.Dequeue();
|
||||
}
|
||||
|
||||
responseAwaiter.SetResult(msg.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IndexOutOfRangeException($"Invalid message kind {msg.Kind}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Message handler for '{msg}' failed with exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!IsDisposed || !(e is SocketException || e.InnerException is SocketException))
|
||||
{
|
||||
Logger.LogError("Unhandled exception in the peer loop", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DoHandshake(string identity)
|
||||
{
|
||||
if (!await WriteLine(handshake.GetHandshakeLine(identity)))
|
||||
{
|
||||
Logger.LogError("Could not write handshake");
|
||||
return false;
|
||||
}
|
||||
|
||||
var readHandshakeTask = ReadLine();
|
||||
|
||||
if (await Task.WhenAny(readHandshakeTask, Task.Delay(8000)) != readHandshakeTask)
|
||||
{
|
||||
Logger.LogError("Timeout waiting for the client handshake");
|
||||
return false;
|
||||
}
|
||||
|
||||
string? peerHandshake = await readHandshakeTask;
|
||||
|
||||
if (peerHandshake == null || !handshake.IsValidPeerHandshake(peerHandshake, out remoteIdentity, Logger))
|
||||
{
|
||||
Logger.LogError("Received invalid handshake: " + peerHandshake);
|
||||
return false;
|
||||
}
|
||||
|
||||
IsConnected = true;
|
||||
Connected?.Invoke();
|
||||
|
||||
Logger.LogInfo("Peer connection started");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<string?> ReadLine()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await clientReader.ReadLineAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (IsDisposed)
|
||||
{
|
||||
var se = e as SocketException ?? e.InnerException as SocketException;
|
||||
if (se != null && se.SocketErrorCode == SocketError.Interrupted)
|
||||
return null;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private Task<bool> WriteMessage(Message message)
|
||||
{
|
||||
Logger.LogDebug($"Sending message: {message}");
|
||||
int bodyLineCount = message.Content.Body.Count(c => c == '\n');
|
||||
|
||||
bodyLineCount += 1; // Extra line break at the end
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine(message.Kind.ToString());
|
||||
builder.AppendLine(message.Id);
|
||||
builder.AppendLine(message.Content.Status.ToString());
|
||||
builder.AppendLine(bodyLineCount.ToString());
|
||||
builder.AppendLine(message.Content.Body);
|
||||
|
||||
return WriteLine(builder.ToString());
|
||||
}
|
||||
|
||||
public async Task<TResponse?> SendRequest<TResponse>(string id, string body)
|
||||
where TResponse : Response, new()
|
||||
{
|
||||
ResponseAwaiter responseAwaiter;
|
||||
|
||||
using (await requestsSem.UseAsync())
|
||||
{
|
||||
bool written = await WriteMessage(new Message(MessageKind.Request, id, new MessageContent(body)));
|
||||
|
||||
if (!written)
|
||||
return null;
|
||||
|
||||
if (!requestAwaiterQueues.TryGetValue(id, out var queue))
|
||||
{
|
||||
queue = new Queue<ResponseAwaiter>();
|
||||
requestAwaiterQueues.Add(id, queue);
|
||||
}
|
||||
|
||||
responseAwaiter = new ResponseAwaiter<TResponse>();
|
||||
queue.Enqueue(responseAwaiter);
|
||||
}
|
||||
|
||||
return (TResponse)await responseAwaiter;
|
||||
}
|
||||
|
||||
private async Task<bool> WriteLine(string text)
|
||||
{
|
||||
if (IsDisposed || !IsTcpClientConnected)
|
||||
return false;
|
||||
|
||||
using (await writeSem.UseAsync())
|
||||
{
|
||||
try
|
||||
{
|
||||
await clientWriter.WriteLineAsync(text);
|
||||
await clientWriter.FlushAsync();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
public void ShutdownSocketSend()
|
||||
{
|
||||
tcpClient.Client.Shutdown(SocketShutdown.Send);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
IsDisposed = true;
|
||||
|
||||
if (IsTcpClientConnected)
|
||||
{
|
||||
if (IsConnected)
|
||||
Disconnected?.Invoke();
|
||||
}
|
||||
|
||||
clientReader.Dispose();
|
||||
clientWriter.Dispose();
|
||||
((IDisposable)tcpClient).Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,130 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GodotTools.IdeMessaging.Requests
|
||||
{
|
||||
public abstract class Request
|
||||
{
|
||||
[JsonIgnore] public string Id { get; }
|
||||
|
||||
protected Request(string id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class Response
|
||||
{
|
||||
[JsonIgnore] public MessageStatus Status { get; set; } = MessageStatus.Ok;
|
||||
}
|
||||
|
||||
public sealed class CodeCompletionRequest : Request
|
||||
{
|
||||
public enum CompletionKind
|
||||
{
|
||||
InputActions = 0,
|
||||
NodePaths,
|
||||
ResourcePaths,
|
||||
ScenePaths,
|
||||
ShaderParams,
|
||||
Signals,
|
||||
ThemeColors,
|
||||
ThemeConstants,
|
||||
ThemeFonts,
|
||||
ThemeStyles
|
||||
}
|
||||
|
||||
public CompletionKind Kind { get; set; }
|
||||
public string ScriptFile { get; set; } = string.Empty;
|
||||
|
||||
public new const string Id = "CodeCompletion";
|
||||
|
||||
public CodeCompletionRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CodeCompletionResponse : Response
|
||||
{
|
||||
public CodeCompletionRequest.CompletionKind Kind;
|
||||
public string ScriptFile { get; set; } = string.Empty;
|
||||
public string[] Suggestions { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed class PlayRequest : Request
|
||||
{
|
||||
public new const string Id = "Play";
|
||||
|
||||
public PlayRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlayResponse : Response
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class StopPlayRequest : Request
|
||||
{
|
||||
public new const string Id = "StopPlay";
|
||||
|
||||
public StopPlayRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StopPlayResponse : Response
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class DebugPlayRequest : Request
|
||||
{
|
||||
public string DebuggerHost { get; set; } = string.Empty;
|
||||
public int DebuggerPort { get; set; }
|
||||
public bool? BuildBeforePlaying { get; set; }
|
||||
|
||||
public new const string Id = "DebugPlay";
|
||||
|
||||
public DebugPlayRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DebugPlayResponse : Response
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class OpenFileRequest : Request
|
||||
{
|
||||
public string File { get; set; } = string.Empty;
|
||||
public int? Line { get; set; }
|
||||
public int? Column { get; set; }
|
||||
|
||||
public new const string Id = "OpenFile";
|
||||
|
||||
public OpenFileRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class OpenFileResponse : Response
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class ReloadScriptsRequest : Request
|
||||
{
|
||||
public new const string Id = "ReloadScripts";
|
||||
|
||||
public ReloadScriptsRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ReloadScriptsResponse : Response
|
||||
{
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using GodotTools.IdeMessaging.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public abstract class ResponseAwaiter : NotifyAwaiter<Response>
|
||||
{
|
||||
public abstract void SetResult(MessageContent content);
|
||||
}
|
||||
|
||||
public class ResponseAwaiter<T> : ResponseAwaiter
|
||||
where T : Response, new()
|
||||
{
|
||||
public override void SetResult(MessageContent content)
|
||||
{
|
||||
if (content.Status == MessageStatus.Ok)
|
||||
SetResult(JsonConvert.DeserializeObject<T>(content.Body)!);
|
||||
else
|
||||
SetResult(new T { Status = content.Status });
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
// ReSharper disable ParameterHidesMember
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace GodotTools.IdeMessaging.Utils
|
||||
{
|
||||
public class NotifyAwaiter<T> : INotifyCompletion
|
||||
{
|
||||
private Action? continuation;
|
||||
private Exception? exception;
|
||||
private T? result;
|
||||
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
public T GetResult()
|
||||
{
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
return result!;
|
||||
}
|
||||
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
if (this.continuation != null)
|
||||
throw new InvalidOperationException("This awaiter already has a continuation.");
|
||||
this.continuation = continuation;
|
||||
}
|
||||
|
||||
public void SetResult(T result)
|
||||
{
|
||||
if (IsCompleted)
|
||||
throw new InvalidOperationException("This awaiter is already completed.");
|
||||
|
||||
IsCompleted = true;
|
||||
this.result = result;
|
||||
|
||||
continuation?.Invoke();
|
||||
}
|
||||
|
||||
public void SetException(Exception exception)
|
||||
{
|
||||
if (IsCompleted)
|
||||
throw new InvalidOperationException("This awaiter is already completed.");
|
||||
|
||||
IsCompleted = true;
|
||||
this.exception = exception;
|
||||
|
||||
continuation?.Invoke();
|
||||
}
|
||||
|
||||
public NotifyAwaiter<T> Reset()
|
||||
{
|
||||
continuation = null;
|
||||
exception = null;
|
||||
result = default(T);
|
||||
IsCompleted = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public NotifyAwaiter<T> GetAwaiter()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GodotTools.IdeMessaging.Utils
|
||||
{
|
||||
public static class SemaphoreExtensions
|
||||
{
|
||||
public static ConfiguredTaskAwaitable<IDisposable> UseAsync(this SemaphoreSlim semaphoreSlim, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var wrapper = new SemaphoreSlimWaitReleaseWrapper(semaphoreSlim, out Task waitAsyncTask, cancellationToken);
|
||||
return waitAsyncTask.ContinueWith<IDisposable>(t => wrapper, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private readonly struct SemaphoreSlimWaitReleaseWrapper : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim semaphoreSlim;
|
||||
|
||||
public SemaphoreSlimWaitReleaseWrapper(SemaphoreSlim semaphoreSlim, out Task waitAsyncTask, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
this.semaphoreSlim = semaphoreSlim;
|
||||
waitAsyncTask = this.semaphoreSlim.WaitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user