[3.2] C#: Rewrite GodotTools messaging protocol
This commit is contained in:
parent
aa57bb0473
commit
fb2e00a854
|
@ -1,33 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace GodotTools.IdeConnection
|
|
||||||
{
|
|
||||||
public class ConsoleLogger : ILogger
|
|
||||||
{
|
|
||||||
public void LogDebug(string message)
|
|
||||||
{
|
|
||||||
Console.WriteLine("DEBUG: " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LogInfo(string message)
|
|
||||||
{
|
|
||||||
Console.WriteLine("INFO: " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LogWarning(string message)
|
|
||||||
{
|
|
||||||
Console.WriteLine("WARN: " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LogError(string message)
|
|
||||||
{
|
|
||||||
Console.WriteLine("ERROR: " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LogError(string message, Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine("EXCEPTION: " + message);
|
|
||||||
Console.WriteLine(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
using System;
|
|
||||||
using Path = System.IO.Path;
|
|
||||||
|
|
||||||
namespace GodotTools.IdeConnection
|
|
||||||
{
|
|
||||||
public class GodotIdeBase : IDisposable
|
|
||||||
{
|
|
||||||
private ILogger logger;
|
|
||||||
|
|
||||||
public ILogger Logger
|
|
||||||
{
|
|
||||||
get => logger ?? (logger = new ConsoleLogger());
|
|
||||||
set => logger = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly string projectMetadataDir;
|
|
||||||
|
|
||||||
protected const string MetaFileName = "ide_server_meta.txt";
|
|
||||||
protected string MetaFilePath => Path.Combine(projectMetadataDir, MetaFileName);
|
|
||||||
|
|
||||||
private GodotIdeConnection connection;
|
|
||||||
protected readonly object ConnectionLock = new object();
|
|
||||||
|
|
||||||
public bool IsDisposed { get; private set; } = false;
|
|
||||||
|
|
||||||
public bool IsConnected => connection != null && !connection.IsDisposed && connection.IsConnected;
|
|
||||||
|
|
||||||
public event Action Connected
|
|
||||||
{
|
|
||||||
add
|
|
||||||
{
|
|
||||||
if (connection != null && !connection.IsDisposed)
|
|
||||||
connection.Connected += value;
|
|
||||||
}
|
|
||||||
remove
|
|
||||||
{
|
|
||||||
if (connection != null && !connection.IsDisposed)
|
|
||||||
connection.Connected -= value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected GodotIdeConnection Connection
|
|
||||||
{
|
|
||||||
get => connection;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
connection?.Dispose();
|
|
||||||
connection = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected GodotIdeBase(string projectMetadataDir)
|
|
||||||
{
|
|
||||||
this.projectMetadataDir = projectMetadataDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void DisposeConnection()
|
|
||||||
{
|
|
||||||
lock (ConnectionLock)
|
|
||||||
{
|
|
||||||
connection?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~GodotIdeBase()
|
|
||||||
{
|
|
||||||
Dispose(disposing: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (IsDisposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
lock (ConnectionLock)
|
|
||||||
{
|
|
||||||
if (IsDisposed) // lock may not be fair
|
|
||||||
return;
|
|
||||||
IsDisposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
connection?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,219 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace GodotTools.IdeConnection
|
|
||||||
{
|
|
||||||
public abstract class GodotIdeClient : GodotIdeBase
|
|
||||||
{
|
|
||||||
protected GodotIdeMetadata GodotIdeMetadata;
|
|
||||||
|
|
||||||
private readonly FileSystemWatcher fsWatcher;
|
|
||||||
|
|
||||||
protected GodotIdeClient(string projectMetadataDir) : base(projectMetadataDir)
|
|
||||||
{
|
|
||||||
messageHandlers = InitializeMessageHandlers();
|
|
||||||
|
|
||||||
// FileSystemWatcher requires an existing directory
|
|
||||||
if (!File.Exists(projectMetadataDir))
|
|
||||||
Directory.CreateDirectory(projectMetadataDir);
|
|
||||||
|
|
||||||
fsWatcher = new FileSystemWatcher(projectMetadataDir, MetaFileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMetaFileChanged(object sender, FileSystemEventArgs e)
|
|
||||||
{
|
|
||||||
if (IsDisposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
lock (ConnectionLock)
|
|
||||||
{
|
|
||||||
if (IsDisposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!File.Exists(MetaFilePath))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var metadata = ReadMetadataFile();
|
|
||||||
|
|
||||||
if (metadata != null && metadata != GodotIdeMetadata)
|
|
||||||
{
|
|
||||||
GodotIdeMetadata = metadata.Value;
|
|
||||||
ConnectToServer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
|
|
||||||
{
|
|
||||||
if (IsDisposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (IsConnected)
|
|
||||||
DisposeConnection();
|
|
||||||
|
|
||||||
// The file may have been re-created
|
|
||||||
|
|
||||||
lock (ConnectionLock)
|
|
||||||
{
|
|
||||||
if (IsDisposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (IsConnected || !File.Exists(MetaFilePath))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var metadata = ReadMetadataFile();
|
|
||||||
|
|
||||||
if (metadata != null)
|
|
||||||
{
|
|
||||||
GodotIdeMetadata = metadata.Value;
|
|
||||||
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 void ConnectToServer()
|
|
||||||
{
|
|
||||||
var tcpClient = new TcpClient();
|
|
||||||
|
|
||||||
Connection = new GodotIdeConnectionClient(tcpClient, HandleMessage);
|
|
||||||
Connection.Logger = Logger;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Logger.LogInfo("Connecting to Godot Ide Server");
|
|
||||||
|
|
||||||
tcpClient.Connect(IPAddress.Loopback, GodotIdeMetadata.Port);
|
|
||||||
|
|
||||||
Logger.LogInfo("Connection open with Godot Ide Server");
|
|
||||||
|
|
||||||
var clientThread = new Thread(Connection.Start)
|
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = "Godot Ide Connection Client"
|
|
||||||
};
|
|
||||||
clientThread.Start();
|
|
||||||
}
|
|
||||||
catch (SocketException e)
|
|
||||||
{
|
|
||||||
if (e.SocketErrorCode == SocketError.ConnectionRefused)
|
|
||||||
Logger.LogError("The connection to the Godot Ide Server was refused");
|
|
||||||
else
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
Logger.LogInfo("Starting Godot Ide Client");
|
|
||||||
|
|
||||||
fsWatcher.Changed += OnMetaFileChanged;
|
|
||||||
fsWatcher.Deleted += OnMetaFileDeleted;
|
|
||||||
fsWatcher.EnableRaisingEvents = true;
|
|
||||||
|
|
||||||
lock (ConnectionLock)
|
|
||||||
{
|
|
||||||
if (IsDisposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!File.Exists(MetaFilePath))
|
|
||||||
{
|
|
||||||
Logger.LogInfo("There is no Godot Ide Server running");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = ReadMetadataFile();
|
|
||||||
|
|
||||||
if (metadata != null)
|
|
||||||
{
|
|
||||||
GodotIdeMetadata = metadata.Value;
|
|
||||||
ConnectToServer();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.LogError("Failed to read Godot Ide metadata file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool WriteMessage(Message message)
|
|
||||||
{
|
|
||||||
return Connection.WriteMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
base.Dispose(disposing);
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
fsWatcher?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual bool HandleMessage(Message message)
|
|
||||||
{
|
|
||||||
if (messageHandlers.TryGetValue(message.Id, out var action))
|
|
||||||
{
|
|
||||||
action(message.Arguments);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<string, Action<string[]>> messageHandlers;
|
|
||||||
|
|
||||||
private Dictionary<string, Action<string[]>> InitializeMessageHandlers()
|
|
||||||
{
|
|
||||||
return new Dictionary<string, Action<string[]>>
|
|
||||||
{
|
|
||||||
["OpenFile"] = args =>
|
|
||||||
{
|
|
||||||
switch (args.Length)
|
|
||||||
{
|
|
||||||
case 1:
|
|
||||||
OpenFile(file: args[0]);
|
|
||||||
return;
|
|
||||||
case 2:
|
|
||||||
OpenFile(file: args[0], line: int.Parse(args[1]));
|
|
||||||
return;
|
|
||||||
case 3:
|
|
||||||
OpenFile(file: args[0], line: int.Parse(args[1]), column: int.Parse(args[2]));
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
throw new ArgumentException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void OpenFile(string file);
|
|
||||||
protected abstract void OpenFile(string file, int line);
|
|
||||||
protected abstract void OpenFile(string file, int line, int column);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,207 +0,0 @@
|
||||||
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<Message, bool> 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<Message, bool> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GodotTools.IdeConnection
|
|
||||||
{
|
|
||||||
public class GodotIdeConnectionClient : GodotIdeConnection
|
|
||||||
{
|
|
||||||
public GodotIdeConnectionClient(TcpClient tcpClient, Func<Message, bool> messageHandler)
|
|
||||||
: base(tcpClient, messageHandler)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool WriteHandshake()
|
|
||||||
{
|
|
||||||
return WriteLine(ClientHandshake);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool IsValidResponseHandshake(string handshakeLine)
|
|
||||||
{
|
|
||||||
return handshakeLine == ServerHandshake;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GodotTools.IdeConnection
|
|
||||||
{
|
|
||||||
public class GodotIdeConnectionServer : GodotIdeConnection
|
|
||||||
{
|
|
||||||
public GodotIdeConnectionServer(TcpClient tcpClient, Func<Message, bool> messageHandler)
|
|
||||||
: base(tcpClient, messageHandler)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool WriteHandshake()
|
|
||||||
{
|
|
||||||
return WriteLine(ServerHandshake);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool IsValidResponseHandshake(string handshakeLine)
|
|
||||||
{
|
|
||||||
return handshakeLine == ClientHandshake;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
|
||||||
<PropertyGroup>
|
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
|
||||||
<ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
|
|
||||||
<OutputType>Library</OutputType>
|
|
||||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
|
||||||
<RootNamespace>GodotTools.IdeConnection</RootNamespace>
|
|
||||||
<AssemblyName>GodotTools.IdeConnection</AssemblyName>
|
|
||||||
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
|
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
<LangVersion>7</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>portable</DebugType>
|
|
||||||
<Optimize>false</Optimize>
|
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugType>portable</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="System" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="ConsoleLogger.cs" />
|
|
||||||
<Compile Include="GodotIdeMetadata.cs" />
|
|
||||||
<Compile Include="GodotIdeBase.cs" />
|
|
||||||
<Compile Include="GodotIdeClient.cs" />
|
|
||||||
<Compile Include="GodotIdeConnection.cs" />
|
|
||||||
<Compile Include="GodotIdeConnectionClient.cs" />
|
|
||||||
<Compile Include="GodotIdeConnectionServer.cs" />
|
|
||||||
<Compile Include="ILogger.cs" />
|
|
||||||
<Compile Include="Message.cs" />
|
|
||||||
<Compile Include="MessageComposer.cs" />
|
|
||||||
<Compile Include="MessageParser.cs" />
|
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
|
||||||
</Project>
|
|
|
@ -1,21 +0,0 @@
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace GodotTools.IdeConnection
|
|
||||||
{
|
|
||||||
public struct Message
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string[] Arguments { get; set; }
|
|
||||||
|
|
||||||
public Message(string id, params string[] arguments)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Arguments = arguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"(Id: '{Id}', Arguments: '{string.Join(",", Arguments)}')";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace GodotTools.IdeConnection
|
|
||||||
{
|
|
||||||
public class MessageComposer
|
|
||||||
{
|
|
||||||
private readonly StringBuilder stringBuilder = new StringBuilder();
|
|
||||||
|
|
||||||
private static readonly char[] CharsToEscape = { '\\', '"' };
|
|
||||||
|
|
||||||
public void AddArgument(string argument)
|
|
||||||
{
|
|
||||||
AddArgument(argument, quoted: argument.Contains(","));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddArgument(string argument, bool quoted)
|
|
||||||
{
|
|
||||||
if (stringBuilder.Length > 0)
|
|
||||||
stringBuilder.Append(',');
|
|
||||||
|
|
||||||
if (quoted)
|
|
||||||
{
|
|
||||||
stringBuilder.Append('"');
|
|
||||||
|
|
||||||
foreach (char @char in argument)
|
|
||||||
{
|
|
||||||
if (CharsToEscape.Contains(@char))
|
|
||||||
stringBuilder.Append('\\');
|
|
||||||
stringBuilder.Append(@char);
|
|
||||||
}
|
|
||||||
|
|
||||||
stringBuilder.Append('"');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stringBuilder.Append(argument);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return stringBuilder.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace GodotTools.IdeConnection
|
|
||||||
{
|
|
||||||
public static class MessageParser
|
|
||||||
{
|
|
||||||
public static bool TryParse(string messageLine, out Message message)
|
|
||||||
{
|
|
||||||
var arguments = new List<string>();
|
|
||||||
var stringBuilder = new StringBuilder();
|
|
||||||
|
|
||||||
bool expectingArgument = true;
|
|
||||||
|
|
||||||
for (int i = 0; i < messageLine.Length; i++)
|
|
||||||
{
|
|
||||||
char @char = messageLine[i];
|
|
||||||
|
|
||||||
if (@char == ',')
|
|
||||||
{
|
|
||||||
if (expectingArgument)
|
|
||||||
arguments.Add(string.Empty);
|
|
||||||
|
|
||||||
expectingArgument = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool quoted = false;
|
|
||||||
|
|
||||||
if (messageLine[i] == '"')
|
|
||||||
{
|
|
||||||
quoted = true;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (i < messageLine.Length)
|
|
||||||
{
|
|
||||||
@char = messageLine[i];
|
|
||||||
|
|
||||||
if (quoted && @char == '"')
|
|
||||||
{
|
|
||||||
i++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@char == '\\')
|
|
||||||
{
|
|
||||||
i++;
|
|
||||||
if (i < messageLine.Length)
|
|
||||||
break;
|
|
||||||
|
|
||||||
stringBuilder.Append(messageLine[i]);
|
|
||||||
}
|
|
||||||
else if (!quoted && @char == ',')
|
|
||||||
{
|
|
||||||
break; // We don't increment the counter to allow the colon to be parsed after this
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stringBuilder.Append(@char);
|
|
||||||
}
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
arguments.Add(stringBuilder.ToString());
|
|
||||||
stringBuilder.Clear();
|
|
||||||
|
|
||||||
expectingArgument = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arguments.Count == 0)
|
|
||||||
{
|
|
||||||
message = new Message();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
message = new Message
|
|
||||||
{
|
|
||||||
Id = arguments[0],
|
|
||||||
Arguments = arguments.Skip(1).ToArray()
|
|
||||||
};
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
|
||||||
// set of attributes. Change these attribute values to modify the information
|
|
||||||
// associated with an assembly.
|
|
||||||
[assembly: AssemblyTitle("GodotTools.IdeConnection")]
|
|
||||||
[assembly: AssemblyDescription("")]
|
|
||||||
[assembly: AssemblyConfiguration("")]
|
|
||||||
[assembly: AssemblyCompany("")]
|
|
||||||
[assembly: AssemblyProduct("")]
|
|
||||||
[assembly: AssemblyCopyright("Godot Engine contributors")]
|
|
||||||
[assembly: AssemblyTrademark("")]
|
|
||||||
[assembly: AssemblyCulture("")]
|
|
||||||
|
|
||||||
// Setting ComVisible to false makes the types in this assembly not visible
|
|
||||||
// to COM components. If you need to access a type in this assembly from
|
|
||||||
// COM, set the ComVisible attribute to true on that type.
|
|
||||||
[assembly: ComVisible(false)]
|
|
||||||
|
|
||||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
|
||||||
[assembly: Guid("92600954-25F0-4291-8E11-1FEE9FC4BE20")]
|
|
||||||
|
|
||||||
// Version information for an assembly consists of the following four values:
|
|
||||||
//
|
|
||||||
// Major Version
|
|
||||||
// Minor Version
|
|
||||||
// Build Number
|
|
||||||
// Revision
|
|
||||||
//
|
|
||||||
// You can specify all the values or you can default the Build and Revision Numbers
|
|
||||||
// by using the '*' as shown below:
|
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
|
||||||
[assembly: AssemblyVersion("1.0.0.0")]
|
|
||||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GodotTools.IdeMessaging.Utils;
|
||||||
|
|
||||||
|
namespace GodotTools.IdeMessaging.CLI
|
||||||
|
{
|
||||||
|
public class ForwarderMessageHandler : IMessageHandler
|
||||||
|
{
|
||||||
|
private readonly StreamWriter outputWriter;
|
||||||
|
private readonly SemaphoreSlim outputWriteSem = new SemaphoreSlim(1);
|
||||||
|
|
||||||
|
public ForwarderMessageHandler(StreamWriter outputWriter)
|
||||||
|
{
|
||||||
|
this.outputWriter = outputWriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
|
||||||
|
{
|
||||||
|
await WriteRequestToOutput(id, content);
|
||||||
|
return new MessageContent(MessageStatus.RequestNotSupported, "null");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WriteRequestToOutput(string id, MessageContent content)
|
||||||
|
{
|
||||||
|
using (await outputWriteSem.UseAsync())
|
||||||
|
{
|
||||||
|
await outputWriter.WriteLineAsync("======= Request =======");
|
||||||
|
await outputWriter.WriteLineAsync(id);
|
||||||
|
await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
|
||||||
|
await outputWriter.WriteLineAsync(content.Body);
|
||||||
|
await outputWriter.WriteLineAsync("=======================");
|
||||||
|
await outputWriter.FlushAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteResponseToOutput(string id, MessageContent content)
|
||||||
|
{
|
||||||
|
using (await outputWriteSem.UseAsync())
|
||||||
|
{
|
||||||
|
await outputWriter.WriteLineAsync("======= Response =======");
|
||||||
|
await outputWriter.WriteLineAsync(id);
|
||||||
|
await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
|
||||||
|
await outputWriter.WriteLineAsync(content.Body);
|
||||||
|
await outputWriter.WriteLineAsync("========================");
|
||||||
|
await outputWriter.FlushAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteLineToOutput(string eventName)
|
||||||
|
{
|
||||||
|
using (await outputWriteSem.UseAsync())
|
||||||
|
await outputWriter.WriteLineAsync($"======= {eventName} =======");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ProjectGuid>{B06C2951-C8E3-4F28-80B2-717CF327EB19}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net472</TargetFramework>
|
||||||
|
<LangVersion>7.2</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,218 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GodotTools.IdeMessaging.Requests;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace GodotTools.IdeMessaging.CLI
|
||||||
|
{
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
private static readonly ILogger Logger = new CustomLogger();
|
||||||
|
|
||||||
|
public static int Main(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mainTask = StartAsync(args, Console.OpenStandardInput(), Console.OpenStandardOutput());
|
||||||
|
mainTask.Wait();
|
||||||
|
return mainTask.Result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError("Unhandled exception: ", ex);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> StartAsync(string[] args, Stream inputStream, Stream outputStream)
|
||||||
|
{
|
||||||
|
var inputReader = new StreamReader(inputStream, Encoding.UTF8);
|
||||||
|
var outputWriter = new StreamWriter(outputStream, Encoding.UTF8);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
Logger.LogError("Expected at least 1 argument");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
string godotProjectDir = args[0];
|
||||||
|
|
||||||
|
if (!Directory.Exists(godotProjectDir))
|
||||||
|
{
|
||||||
|
Logger.LogError($"The specified Godot project directory does not exist: {godotProjectDir}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var forwarder = new ForwarderMessageHandler(outputWriter);
|
||||||
|
|
||||||
|
using (var fwdClient = new Client("VisualStudioCode", godotProjectDir, forwarder, Logger))
|
||||||
|
{
|
||||||
|
fwdClient.Start();
|
||||||
|
|
||||||
|
// ReSharper disable AccessToDisposedClosure
|
||||||
|
fwdClient.Connected += async () => await forwarder.WriteLineToOutput("Event=Connected");
|
||||||
|
fwdClient.Disconnected += async () => await forwarder.WriteLineToOutput("Event=Disconnected");
|
||||||
|
// ReSharper restore AccessToDisposedClosure
|
||||||
|
|
||||||
|
// TODO: Await connected with timeout
|
||||||
|
|
||||||
|
while (!fwdClient.IsDisposed)
|
||||||
|
{
|
||||||
|
string firstLine = await inputReader.ReadLineAsync();
|
||||||
|
|
||||||
|
if (firstLine == null || firstLine == "QUIT")
|
||||||
|
goto ExitMainLoop;
|
||||||
|
|
||||||
|
string messageId = firstLine;
|
||||||
|
|
||||||
|
string messageArgcLine = await inputReader.ReadLineAsync();
|
||||||
|
|
||||||
|
if (messageArgcLine == null)
|
||||||
|
{
|
||||||
|
Logger.LogInfo("EOF when expecting argument count");
|
||||||
|
goto ExitMainLoop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(messageArgcLine, out int messageArgc))
|
||||||
|
{
|
||||||
|
Logger.LogError("Received invalid line for argument count: " + firstLine);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < messageArgc; i++)
|
||||||
|
{
|
||||||
|
string bodyLine = await inputReader.ReadLineAsync();
|
||||||
|
|
||||||
|
if (bodyLine == null)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"EOF when expecting body line #{i + 1}");
|
||||||
|
goto ExitMainLoop;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.AppendLine(bodyLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await SendRequest(fwdClient, messageId, new MessageContent(MessageStatus.Ok, body.ToString()));
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to write message to the server: {messageId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var content = new MessageContent(response.Status, JsonConvert.SerializeObject(response));
|
||||||
|
await forwarder.WriteResponseToOutput(messageId, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExitMainLoop:
|
||||||
|
|
||||||
|
await forwarder.WriteLineToOutput("Event=Quit");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError("Unhandled exception", e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Response> SendRequest(Client client, string id, MessageContent content)
|
||||||
|
{
|
||||||
|
var handlers = new Dictionary<string, Func<Task<Response>>>
|
||||||
|
{
|
||||||
|
[PlayRequest.Id] = async () =>
|
||||||
|
{
|
||||||
|
var request = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
|
||||||
|
return await client.SendRequest<PlayResponse>(request);
|
||||||
|
},
|
||||||
|
[DebugPlayRequest.Id] = async () =>
|
||||||
|
{
|
||||||
|
var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
|
||||||
|
return await client.SendRequest<DebugPlayResponse>(request);
|
||||||
|
},
|
||||||
|
[ReloadScriptsRequest.Id] = async () =>
|
||||||
|
{
|
||||||
|
var request = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
|
||||||
|
return await client.SendRequest<ReloadScriptsResponse>(request);
|
||||||
|
},
|
||||||
|
[CodeCompletionRequest.Id] = async () =>
|
||||||
|
{
|
||||||
|
var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
|
||||||
|
return await client.SendRequest<CodeCompletionResponse>(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (handlers.TryGetValue(id, out var handler))
|
||||||
|
return await handler();
|
||||||
|
|
||||||
|
Console.WriteLine("INVALID REQUEST");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomLogger : ILogger
|
||||||
|
{
|
||||||
|
private static string ThisAppPath => Assembly.GetExecutingAssembly().Location;
|
||||||
|
private static string ThisAppPathWithoutExtension => Path.ChangeExtension(ThisAppPath, null);
|
||||||
|
|
||||||
|
private static readonly string LogPath = $"{ThisAppPathWithoutExtension}.log";
|
||||||
|
|
||||||
|
private static StreamWriter NewWriter() => new StreamWriter(LogPath, append: true, encoding: Encoding.UTF8);
|
||||||
|
|
||||||
|
private static void Log(StreamWriter writer, string message)
|
||||||
|
{
|
||||||
|
writer.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}: {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogDebug(string message)
|
||||||
|
{
|
||||||
|
using (var writer = NewWriter())
|
||||||
|
{
|
||||||
|
Log(writer, "DEBUG: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogInfo(string message)
|
||||||
|
{
|
||||||
|
using (var writer = NewWriter())
|
||||||
|
{
|
||||||
|
Log(writer, "INFO: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogWarning(string message)
|
||||||
|
{
|
||||||
|
using (var writer = NewWriter())
|
||||||
|
{
|
||||||
|
Log(writer, "WARN: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogError(string message)
|
||||||
|
{
|
||||||
|
using (var writer = NewWriter())
|
||||||
|
{
|
||||||
|
Log(writer, "ERROR: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogError(string message, Exception e)
|
||||||
|
{
|
||||||
|
using (var writer = NewWriter())
|
||||||
|
{
|
||||||
|
Log(writer, "EXCEPTION: " + message + '\n' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
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, 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
namespace GodotTools.IdeConnection
|
namespace GodotTools.IdeMessaging
|
||||||
{
|
{
|
||||||
public struct GodotIdeMetadata
|
public readonly struct GodotIdeMetadata
|
||||||
{
|
{
|
||||||
public int Port { get; }
|
public int Port { get; }
|
||||||
public string EditorExecutablePath { get; }
|
public string EditorExecutablePath { get; }
|
||||||
|
|
||||||
|
public const string DefaultFileName = "ide_messaging_meta.txt";
|
||||||
|
|
||||||
public GodotIdeMetadata(int port, string editorExecutablePath)
|
public GodotIdeMetadata(int port, string editorExecutablePath)
|
||||||
{
|
{
|
||||||
Port = port;
|
Port = port;
|
|
@ -0,0 +1,24 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>7.2</LangVersion>
|
||||||
|
<PackageId>GodotTools.IdeMessaging</PackageId>
|
||||||
|
<Version>1.1.0</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>
|
||||||
|
<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="12.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace GodotTools.IdeMessaging
|
||||||
|
{
|
||||||
|
public interface IHandshake
|
||||||
|
{
|
||||||
|
string GetHandshakeLine(string identity);
|
||||||
|
bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace GodotTools.IdeConnection
|
namespace GodotTools.IdeMessaging
|
||||||
{
|
{
|
||||||
public interface ILogger
|
public interface ILogger
|
||||||
{
|
{
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,302 @@
|
||||||
|
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 = string.Empty;
|
||||||
|
public string RemoteIdentity => remoteIdentity;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Exception thrown from message handler. Message: {msg}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
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 (handshake == 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 (clientWriter == null || 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,116 @@
|
||||||
|
// ReSharper disable ClassNeverInstantiated.Global
|
||||||
|
// ReSharper disable UnusedMember.Global
|
||||||
|
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
public new const string Id = "CodeCompletion";
|
||||||
|
|
||||||
|
public CodeCompletionRequest() : base(Id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CodeCompletionResponse : Response
|
||||||
|
{
|
||||||
|
public CodeCompletionRequest.CompletionKind Kind;
|
||||||
|
public string ScriptFile { get; set; }
|
||||||
|
public string[] Suggestions { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayRequest : Request
|
||||||
|
{
|
||||||
|
public new const string Id = "Play";
|
||||||
|
|
||||||
|
public PlayRequest() : base(Id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayResponse : Response
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DebugPlayRequest : Request
|
||||||
|
{
|
||||||
|
public string DebuggerHost { get; set; }
|
||||||
|
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; }
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace GodotTools.Utils
|
namespace GodotTools.IdeMessaging.Utils
|
||||||
{
|
{
|
||||||
public sealed class NotifyAwaiter<T> : INotifyCompletion
|
public class NotifyAwaiter<T> : INotifyCompletion
|
||||||
{
|
{
|
||||||
private Action continuation;
|
private Action continuation;
|
||||||
private Exception exception;
|
private Exception exception;
|
|
@ -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 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,8 +9,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Core", "GodotToo
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.BuildLogger", "GodotTools.BuildLogger\GodotTools.BuildLogger.csproj", "{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.BuildLogger", "GodotTools.BuildLogger\GodotTools.BuildLogger.csproj", "{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.IdeConnection", "GodotTools.IdeConnection\GodotTools.IdeConnection.csproj", "{92600954-25F0-4291-8E11-1FEE9FC4BE20}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -33,9 +31,5 @@ Global
|
||||||
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{92600954-25F0-4291-8E11-1FEE9FC4BE20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{92600954-25F0-4291-8E11-1FEE9FC4BE20}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{92600954-25F0-4291-8E11-1FEE9FC4BE20}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{92600954-25F0-4291-8E11-1FEE9FC4BE20}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
@ -219,7 +219,7 @@ namespace GodotTools
|
||||||
if (File.Exists(editorScriptsMetadataPath))
|
if (File.Exists(editorScriptsMetadataPath))
|
||||||
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
|
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
|
||||||
|
|
||||||
var currentPlayRequest = GodotSharpEditor.Instance.GodotIdeManager.GodotIdeServer.CurrentPlayRequest;
|
var currentPlayRequest = GodotSharpEditor.Instance.CurrentPlaySettings;
|
||||||
|
|
||||||
if (currentPlayRequest != null)
|
if (currentPlayRequest != null)
|
||||||
{
|
{
|
||||||
|
@ -233,7 +233,8 @@ namespace GodotTools
|
||||||
",server=n");
|
",server=n");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true; // Requested play from an external editor/IDE which already built the project
|
if (!currentPlayRequest.Value.BuildBeforePlaying)
|
||||||
|
return true; // Requested play from an external editor/IDE which already built the project
|
||||||
}
|
}
|
||||||
|
|
||||||
var godotDefines = new[]
|
var godotDefines = new[]
|
||||||
|
|
|
@ -37,6 +37,8 @@ namespace GodotTools
|
||||||
|
|
||||||
public BottomPanel BottomPanel { get; private set; }
|
public BottomPanel BottomPanel { get; private set; }
|
||||||
|
|
||||||
|
public PlaySettings? CurrentPlaySettings { get; set; }
|
||||||
|
|
||||||
public static string ProjectAssemblyName
|
public static string ProjectAssemblyName
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -240,12 +242,12 @@ namespace GodotTools
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public Error OpenInExternalEditor(Script script, int line, int col)
|
public Error OpenInExternalEditor(Script script, int line, int col)
|
||||||
{
|
{
|
||||||
var editor = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor");
|
var editorId = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor");
|
||||||
|
|
||||||
switch (editor)
|
switch (editorId)
|
||||||
{
|
{
|
||||||
case ExternalEditorId.None:
|
case ExternalEditorId.None:
|
||||||
// Tells the caller to fallback to the global external editor settings or the built-in editor
|
// Not an error. Tells the caller to fallback to the global external editor settings or the built-in editor.
|
||||||
return Error.Unavailable;
|
return Error.Unavailable;
|
||||||
case ExternalEditorId.VisualStudio:
|
case ExternalEditorId.VisualStudio:
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
|
@ -261,10 +263,14 @@ namespace GodotTools
|
||||||
{
|
{
|
||||||
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
|
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
|
||||||
|
|
||||||
if (line >= 0)
|
GodotIdeManager.LaunchIdeAsync().ContinueWith(launchTask =>
|
||||||
GodotIdeManager.SendOpenFile(scriptPath, line + 1, col);
|
{
|
||||||
else
|
var editorPick = launchTask.Result;
|
||||||
GodotIdeManager.SendOpenFile(scriptPath);
|
if (line >= 0)
|
||||||
|
editorPick?.SendOpenFile(scriptPath, line + 1, col);
|
||||||
|
else
|
||||||
|
editorPick?.SendOpenFile(scriptPath);
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -312,7 +318,7 @@ namespace GodotTools
|
||||||
if (line >= 0)
|
if (line >= 0)
|
||||||
{
|
{
|
||||||
args.Add("-g");
|
args.Add("-g");
|
||||||
args.Add($"{scriptPath}:{line + 1}:{col}");
|
args.Add($"{scriptPath}:{line}:{col}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -31,6 +31,10 @@
|
||||||
<ConsolePause>false</ConsolePause>
|
<ConsolePause>false</ConsolePause>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="GodotTools.IdeMessaging, Version=1.1.0.0, Culture=neutral, PublicKeyToken=null">
|
||||||
|
<HintPath>..\packages\GodotTools.IdeMessaging.1.1.0\lib\netstandard2.0\GodotTools.IdeMessaging.dll</HintPath>
|
||||||
|
<Private>True</Private>
|
||||||
|
</Reference>
|
||||||
<Reference Include="JetBrains.Annotations, Version=2019.1.3.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325">
|
<Reference Include="JetBrains.Annotations, Version=2019.1.3.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325">
|
||||||
<HintPath>..\packages\JetBrains.Annotations.2019.1.3\lib\net20\JetBrains.Annotations.dll</HintPath>
|
<HintPath>..\packages\JetBrains.Annotations.2019.1.3\lib\net20\JetBrains.Annotations.dll</HintPath>
|
||||||
<Private>True</Private>
|
<Private>True</Private>
|
||||||
|
@ -56,7 +60,7 @@
|
||||||
<Compile Include="Export\XcodeHelper.cs" />
|
<Compile Include="Export\XcodeHelper.cs" />
|
||||||
<Compile Include="ExternalEditorId.cs" />
|
<Compile Include="ExternalEditorId.cs" />
|
||||||
<Compile Include="Ides\GodotIdeManager.cs" />
|
<Compile Include="Ides\GodotIdeManager.cs" />
|
||||||
<Compile Include="Ides\GodotIdeServer.cs" />
|
<Compile Include="Ides\MessagingServer.cs" />
|
||||||
<Compile Include="Ides\MonoDevelop\EditorId.cs" />
|
<Compile Include="Ides\MonoDevelop\EditorId.cs" />
|
||||||
<Compile Include="Ides\MonoDevelop\Instance.cs" />
|
<Compile Include="Ides\MonoDevelop\Instance.cs" />
|
||||||
<Compile Include="Ides\Rider\RiderPathLocator.cs" />
|
<Compile Include="Ides\Rider\RiderPathLocator.cs" />
|
||||||
|
@ -70,7 +74,6 @@
|
||||||
<Compile Include="Build\BuildSystem.cs" />
|
<Compile Include="Build\BuildSystem.cs" />
|
||||||
<Compile Include="Utils\Directory.cs" />
|
<Compile Include="Utils\Directory.cs" />
|
||||||
<Compile Include="Utils\File.cs" />
|
<Compile Include="Utils\File.cs" />
|
||||||
<Compile Include="Utils\NotifyAwaiter.cs" />
|
|
||||||
<Compile Include="Utils\OS.cs" />
|
<Compile Include="Utils\OS.cs" />
|
||||||
<Compile Include="GodotSharpEditor.cs" />
|
<Compile Include="GodotSharpEditor.cs" />
|
||||||
<Compile Include="BuildManager.cs" />
|
<Compile Include="BuildManager.cs" />
|
||||||
|
@ -79,6 +82,7 @@
|
||||||
<Compile Include="BuildTab.cs" />
|
<Compile Include="BuildTab.cs" />
|
||||||
<Compile Include="BottomPanel.cs" />
|
<Compile Include="BottomPanel.cs" />
|
||||||
<Compile Include="CsProjOperations.cs" />
|
<Compile Include="CsProjOperations.cs" />
|
||||||
|
<Compile Include="PlaySettings.cs" />
|
||||||
<Compile Include="Utils\CollectionExtensions.cs" />
|
<Compile Include="Utils\CollectionExtensions.cs" />
|
||||||
<Compile Include="Utils\User32Dll.cs" />
|
<Compile Include="Utils\User32Dll.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -87,10 +91,6 @@
|
||||||
<Project>{6ce9a984-37b1-4f8a-8fe9-609f05f071b3}</Project>
|
<Project>{6ce9a984-37b1-4f8a-8fe9-609f05f071b3}</Project>
|
||||||
<Name>GodotTools.BuildLogger</Name>
|
<Name>GodotTools.BuildLogger</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<ProjectReference Include="..\GodotTools.IdeConnection\GodotTools.IdeConnection.csproj">
|
|
||||||
<Project>{92600954-25f0-4291-8e11-1fee9fc4be20}</Project>
|
|
||||||
<Name>GodotTools.IdeConnection</Name>
|
|
||||||
</ProjectReference>
|
|
||||||
<ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj">
|
<ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj">
|
||||||
<Project>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</Project>
|
<Project>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</Project>
|
||||||
<Name>GodotTools.ProjectEditor</Name>
|
<Name>GodotTools.ProjectEditor</Name>
|
||||||
|
|
|
@ -1,73 +1,104 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Godot;
|
using Godot;
|
||||||
using GodotTools.IdeConnection;
|
using GodotTools.IdeMessaging;
|
||||||
|
using GodotTools.IdeMessaging.Requests;
|
||||||
using GodotTools.Internals;
|
using GodotTools.Internals;
|
||||||
|
|
||||||
namespace GodotTools.Ides
|
namespace GodotTools.Ides
|
||||||
{
|
{
|
||||||
public class GodotIdeManager : Node, ISerializationListener
|
public sealed class GodotIdeManager : Node, ISerializationListener
|
||||||
{
|
{
|
||||||
public GodotIdeServer GodotIdeServer { get; private set; }
|
private MessagingServer MessagingServer { get; set; }
|
||||||
|
|
||||||
private MonoDevelop.Instance monoDevelInstance;
|
private MonoDevelop.Instance monoDevelInstance;
|
||||||
private MonoDevelop.Instance vsForMacInstance;
|
private MonoDevelop.Instance vsForMacInstance;
|
||||||
|
|
||||||
private GodotIdeServer GetRunningServer()
|
private MessagingServer GetRunningOrNewServer()
|
||||||
{
|
{
|
||||||
if (GodotIdeServer != null && !GodotIdeServer.IsDisposed)
|
if (MessagingServer != null && !MessagingServer.IsDisposed)
|
||||||
return GodotIdeServer;
|
return MessagingServer;
|
||||||
StartServer();
|
|
||||||
return GodotIdeServer;
|
MessagingServer?.Dispose();
|
||||||
|
MessagingServer = new MessagingServer(OS.GetExecutablePath(), ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir), new GodotLogger());
|
||||||
|
|
||||||
|
_ = MessagingServer.Listen();
|
||||||
|
|
||||||
|
return MessagingServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
StartServer();
|
_ = GetRunningOrNewServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnBeforeSerialize()
|
public void OnBeforeSerialize()
|
||||||
{
|
{
|
||||||
GodotIdeServer?.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnAfterDeserialize()
|
public void OnAfterDeserialize()
|
||||||
{
|
{
|
||||||
StartServer();
|
_ = GetRunningOrNewServer();
|
||||||
}
|
|
||||||
|
|
||||||
private ILogger logger;
|
|
||||||
|
|
||||||
protected ILogger Logger
|
|
||||||
{
|
|
||||||
get => logger ?? (logger = new GodotLogger());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartServer()
|
|
||||||
{
|
|
||||||
GodotIdeServer?.Dispose();
|
|
||||||
GodotIdeServer = new GodotIdeServer(LaunchIde,
|
|
||||||
OS.GetExecutablePath(),
|
|
||||||
ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir));
|
|
||||||
|
|
||||||
GodotIdeServer.Logger = Logger;
|
|
||||||
|
|
||||||
GodotIdeServer.StartServer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|
||||||
GodotIdeServer?.Dispose();
|
if (disposing)
|
||||||
|
{
|
||||||
|
MessagingServer?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LaunchIde()
|
private string GetExternalEditorIdentity(ExternalEditorId editorId)
|
||||||
{
|
{
|
||||||
var editor = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
|
// Manually convert to string to avoid breaking compatibility in case we rename the enum fields.
|
||||||
.GetEditorSettings().GetSetting("mono/editor/external_editor");
|
switch (editorId)
|
||||||
|
{
|
||||||
|
case ExternalEditorId.None:
|
||||||
|
return null;
|
||||||
|
case ExternalEditorId.VisualStudio:
|
||||||
|
return "VisualStudio";
|
||||||
|
case ExternalEditorId.VsCode:
|
||||||
|
return "VisualStudioCode";
|
||||||
|
case ExternalEditorId.Rider:
|
||||||
|
return "Rider";
|
||||||
|
case ExternalEditorId.VisualStudioForMac:
|
||||||
|
return "VisualStudioForMac";
|
||||||
|
case ExternalEditorId.MonoDevelop:
|
||||||
|
return "MonoDevelop";
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (editor)
|
public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000)
|
||||||
|
{
|
||||||
|
var editorId = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
|
||||||
|
.GetEditorSettings().GetSetting("mono/editor/external_editor");
|
||||||
|
string editorIdentity = GetExternalEditorIdentity(editorId);
|
||||||
|
|
||||||
|
var runningServer = GetRunningOrNewServer();
|
||||||
|
|
||||||
|
if (runningServer.IsAnyConnected(editorIdentity))
|
||||||
|
return new EditorPick(editorIdentity);
|
||||||
|
|
||||||
|
LaunchIde(editorId, editorIdentity);
|
||||||
|
|
||||||
|
var timeoutTask = Task.Delay(millisecondsTimeout);
|
||||||
|
var completedTask = await Task.WhenAny(timeoutTask, runningServer.AwaitClientConnected(editorIdentity));
|
||||||
|
|
||||||
|
if (completedTask != timeoutTask)
|
||||||
|
return new EditorPick(editorIdentity);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LaunchIde(ExternalEditorId editorId, string editorIdentity)
|
||||||
|
{
|
||||||
|
switch (editorId)
|
||||||
{
|
{
|
||||||
case ExternalEditorId.None:
|
case ExternalEditorId.None:
|
||||||
case ExternalEditorId.VisualStudio:
|
case ExternalEditorId.VisualStudio:
|
||||||
|
@ -80,14 +111,14 @@ namespace GodotTools.Ides
|
||||||
{
|
{
|
||||||
MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath)
|
MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath)
|
||||||
{
|
{
|
||||||
if (Utils.OS.IsOSX && editor == ExternalEditorId.VisualStudioForMac)
|
if (Utils.OS.IsOSX && editorId == ExternalEditorId.VisualStudioForMac)
|
||||||
{
|
{
|
||||||
vsForMacInstance = vsForMacInstance ??
|
vsForMacInstance = (vsForMacInstance?.IsDisposed ?? true ? null : vsForMacInstance) ??
|
||||||
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac);
|
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac);
|
||||||
return vsForMacInstance;
|
return vsForMacInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
monoDevelInstance = monoDevelInstance ??
|
monoDevelInstance = (monoDevelInstance?.IsDisposed ?? true ? null : monoDevelInstance) ??
|
||||||
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop);
|
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop);
|
||||||
return monoDevelInstance;
|
return monoDevelInstance;
|
||||||
}
|
}
|
||||||
|
@ -96,12 +127,25 @@ namespace GodotTools.Ides
|
||||||
{
|
{
|
||||||
var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath);
|
var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath);
|
||||||
|
|
||||||
if (!instance.IsRunning)
|
if (instance.IsRunning && !GetRunningOrNewServer().IsAnyConnected(editorIdentity))
|
||||||
|
{
|
||||||
|
// After launch we wait up to 30 seconds for the IDE to connect to our messaging server.
|
||||||
|
var waitAfterLaunch = TimeSpan.FromSeconds(30);
|
||||||
|
var timeSinceLaunch = DateTime.Now - instance.LaunchTime;
|
||||||
|
if (timeSinceLaunch > waitAfterLaunch)
|
||||||
|
{
|
||||||
|
instance.Dispose();
|
||||||
|
instance.Execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!instance.IsRunning)
|
||||||
|
{
|
||||||
instance.Execute();
|
instance.Execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
string editorName = editor == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
|
string editorName = editorId == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
|
||||||
GD.PushError($"Cannot find code editor: {editorName}");
|
GD.PushError($"Cannot find code editor: {editorName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,25 +157,44 @@ namespace GodotTools.Ides
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteMessage(string id, params string[] arguments)
|
public struct EditorPick
|
||||||
{
|
{
|
||||||
GetRunningServer().WriteMessage(new Message(id, arguments));
|
private readonly string identity;
|
||||||
|
|
||||||
|
public EditorPick(string identity)
|
||||||
|
{
|
||||||
|
this.identity = identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAnyConnected() =>
|
||||||
|
GodotSharpEditor.Instance.GodotIdeManager.GetRunningOrNewServer().IsAnyConnected(identity);
|
||||||
|
|
||||||
|
private void SendRequest<TResponse>(Request request)
|
||||||
|
where TResponse : Response, new()
|
||||||
|
{
|
||||||
|
// Logs an error if no client is connected with the specified identity
|
||||||
|
GodotSharpEditor.Instance.GodotIdeManager
|
||||||
|
.GetRunningOrNewServer()
|
||||||
|
.BroadcastRequest<TResponse>(identity, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendOpenFile(string file)
|
||||||
|
{
|
||||||
|
SendRequest<OpenFileResponse>(new OpenFileRequest {File = file});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendOpenFile(string file, int line)
|
||||||
|
{
|
||||||
|
SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendOpenFile(string file, int line, int column)
|
||||||
|
{
|
||||||
|
SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line, Column = column});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SendOpenFile(string file)
|
public EditorPick PickEditor(ExternalEditorId editorId) => new EditorPick(GetExternalEditorIdentity(editorId));
|
||||||
{
|
|
||||||
WriteMessage("OpenFile", file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendOpenFile(string file, int line)
|
|
||||||
{
|
|
||||||
WriteMessage("OpenFile", file, line.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendOpenFile(string file, int line, int column)
|
|
||||||
{
|
|
||||||
WriteMessage("OpenFile", file, line.ToString(), column.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private class GodotLogger : ILogger
|
private class GodotLogger : ILogger
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,212 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using GodotTools.IdeConnection;
|
|
||||||
using GodotTools.Internals;
|
|
||||||
using GodotTools.Utils;
|
|
||||||
using Directory = System.IO.Directory;
|
|
||||||
using File = System.IO.File;
|
|
||||||
using Thread = System.Threading.Thread;
|
|
||||||
|
|
||||||
namespace GodotTools.Ides
|
|
||||||
{
|
|
||||||
public class GodotIdeServer : GodotIdeBase
|
|
||||||
{
|
|
||||||
private readonly TcpListener listener;
|
|
||||||
private readonly FileStream metaFile;
|
|
||||||
private readonly Action launchIdeAction;
|
|
||||||
private readonly NotifyAwaiter<bool> clientConnectedAwaiter = new NotifyAwaiter<bool>();
|
|
||||||
|
|
||||||
private async Task<bool> AwaitClientConnected()
|
|
||||||
{
|
|
||||||
return await clientConnectedAwaiter.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
public GodotIdeServer(Action launchIdeAction, string editorExecutablePath, string projectMetadataDir)
|
|
||||||
: base(projectMetadataDir)
|
|
||||||
{
|
|
||||||
messageHandlers = InitializeMessageHandlers();
|
|
||||||
|
|
||||||
this.launchIdeAction = launchIdeAction;
|
|
||||||
|
|
||||||
// Make sure the directory exists
|
|
||||||
Directory.CreateDirectory(projectMetadataDir);
|
|
||||||
|
|
||||||
// The Godot editor's file system thread can keep the file open for writing, so we are forced to allow write sharing...
|
|
||||||
const FileShare metaFileShare = FileShare.ReadWrite;
|
|
||||||
|
|
||||||
metaFile = File.Open(MetaFilePath, FileMode.Create, FileAccess.Write, metaFileShare);
|
|
||||||
|
|
||||||
listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0));
|
|
||||||
listener.Start();
|
|
||||||
|
|
||||||
int port = ((IPEndPoint)listener.Server.LocalEndPoint).Port;
|
|
||||||
using (var metaFileWriter = new StreamWriter(metaFile, Encoding.UTF8))
|
|
||||||
{
|
|
||||||
metaFileWriter.WriteLine(port);
|
|
||||||
metaFileWriter.WriteLine(editorExecutablePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
StartServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartServer()
|
|
||||||
{
|
|
||||||
var serverThread = new Thread(RunServerThread) { Name = "Godot Ide Connection Server" };
|
|
||||||
serverThread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RunServerThread()
|
|
||||||
{
|
|
||||||
SynchronizationContext.SetSynchronizationContext(Godot.Dispatcher.SynchronizationContext);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (!IsDisposed)
|
|
||||||
{
|
|
||||||
TcpClient tcpClient = listener.AcceptTcpClient();
|
|
||||||
|
|
||||||
Logger.LogInfo("Connection open with Ide Client");
|
|
||||||
|
|
||||||
lock (ConnectionLock)
|
|
||||||
{
|
|
||||||
Connection = new GodotIdeConnectionServer(tcpClient, HandleMessage);
|
|
||||||
Connection.Logger = Logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
Connected += () => clientConnectedAwaiter.SetResult(true);
|
|
||||||
|
|
||||||
Connection.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted))
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void WriteMessage(Message message)
|
|
||||||
{
|
|
||||||
async Task LaunchIde()
|
|
||||||
{
|
|
||||||
if (IsConnected)
|
|
||||||
return;
|
|
||||||
|
|
||||||
launchIdeAction();
|
|
||||||
await Task.WhenAny(Task.Delay(10000), AwaitClientConnected());
|
|
||||||
}
|
|
||||||
|
|
||||||
await LaunchIde();
|
|
||||||
|
|
||||||
if (!IsConnected)
|
|
||||||
{
|
|
||||||
Logger.LogError("Cannot write message: Godot Ide Server not connected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Connection.WriteMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
base.Dispose(disposing);
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
listener?.Stop();
|
|
||||||
|
|
||||||
metaFile?.Dispose();
|
|
||||||
|
|
||||||
File.Delete(MetaFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual bool HandleMessage(Message message)
|
|
||||||
{
|
|
||||||
if (messageHandlers.TryGetValue(message.Id, out var action))
|
|
||||||
{
|
|
||||||
action(message.Arguments);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<string, Action<string[]>> messageHandlers;
|
|
||||||
|
|
||||||
private Dictionary<string, Action<string[]>> InitializeMessageHandlers()
|
|
||||||
{
|
|
||||||
return new Dictionary<string, Action<string[]>>
|
|
||||||
{
|
|
||||||
["Play"] = args =>
|
|
||||||
{
|
|
||||||
switch (args.Length)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
Play();
|
|
||||||
return;
|
|
||||||
case 2:
|
|
||||||
Play(debuggerHost: args[0], debuggerPort: int.Parse(args[1]));
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
throw new ArgumentException();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
["ReloadScripts"] = args => ReloadScripts()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DispatchToMainThread(Action action)
|
|
||||||
{
|
|
||||||
var d = new SendOrPostCallback(state => action());
|
|
||||||
Godot.Dispatcher.SynchronizationContext.Post(d, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Play()
|
|
||||||
{
|
|
||||||
DispatchToMainThread(() =>
|
|
||||||
{
|
|
||||||
CurrentPlayRequest = new PlayRequest();
|
|
||||||
Internal.EditorRunPlay();
|
|
||||||
CurrentPlayRequest = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Play(string debuggerHost, int debuggerPort)
|
|
||||||
{
|
|
||||||
DispatchToMainThread(() =>
|
|
||||||
{
|
|
||||||
CurrentPlayRequest = new PlayRequest(debuggerHost, debuggerPort);
|
|
||||||
Internal.EditorRunPlay();
|
|
||||||
CurrentPlayRequest = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReloadScripts()
|
|
||||||
{
|
|
||||||
DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayRequest? CurrentPlayRequest { get; private set; }
|
|
||||||
|
|
||||||
public struct PlayRequest
|
|
||||||
{
|
|
||||||
public bool HasDebugger { get; }
|
|
||||||
public string DebuggerHost { get; }
|
|
||||||
public int DebuggerPort { get; }
|
|
||||||
|
|
||||||
public PlayRequest(string debuggerHost, int debuggerPort)
|
|
||||||
{
|
|
||||||
HasDebugger = true;
|
|
||||||
DebuggerHost = debuggerHost;
|
|
||||||
DebuggerPort = debuggerPort;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,360 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GodotTools.IdeMessaging;
|
||||||
|
using GodotTools.IdeMessaging.Requests;
|
||||||
|
using GodotTools.IdeMessaging.Utils;
|
||||||
|
using GodotTools.Internals;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Directory = System.IO.Directory;
|
||||||
|
using File = System.IO.File;
|
||||||
|
|
||||||
|
namespace GodotTools.Ides
|
||||||
|
{
|
||||||
|
public sealed class MessagingServer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger logger;
|
||||||
|
|
||||||
|
private readonly FileStream metaFile;
|
||||||
|
private string MetaFilePath { get; }
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim peersSem = new SemaphoreSlim(1);
|
||||||
|
|
||||||
|
private readonly TcpListener listener;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, Queue<NotifyAwaiter<bool>>> clientConnectedAwaiters = new Dictionary<string, Queue<NotifyAwaiter<bool>>>();
|
||||||
|
private readonly Dictionary<string, Queue<NotifyAwaiter<bool>>> clientDisconnectedAwaiters = new Dictionary<string, Queue<NotifyAwaiter<bool>>>();
|
||||||
|
|
||||||
|
public async Task<bool> AwaitClientConnected(string identity)
|
||||||
|
{
|
||||||
|
if (!clientConnectedAwaiters.TryGetValue(identity, out var queue))
|
||||||
|
{
|
||||||
|
queue = new Queue<NotifyAwaiter<bool>>();
|
||||||
|
clientConnectedAwaiters.Add(identity, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
var awaiter = new NotifyAwaiter<bool>();
|
||||||
|
queue.Enqueue(awaiter);
|
||||||
|
return await awaiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AwaitClientDisconnected(string identity)
|
||||||
|
{
|
||||||
|
if (!clientDisconnectedAwaiters.TryGetValue(identity, out var queue))
|
||||||
|
{
|
||||||
|
queue = new Queue<NotifyAwaiter<bool>>();
|
||||||
|
clientDisconnectedAwaiters.Add(identity, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
var awaiter = new NotifyAwaiter<bool>();
|
||||||
|
queue.Enqueue(awaiter);
|
||||||
|
return await awaiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
|
public bool IsAnyConnected(string identity) => string.IsNullOrEmpty(identity) ?
|
||||||
|
Peers.Count > 0 :
|
||||||
|
Peers.Any(c => c.RemoteIdentity == identity);
|
||||||
|
|
||||||
|
private List<Peer> Peers { get; } = new List<Peer>();
|
||||||
|
|
||||||
|
~MessagingServer()
|
||||||
|
{
|
||||||
|
Dispose(disposing: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Dispose()
|
||||||
|
{
|
||||||
|
if (IsDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (await peersSem.UseAsync())
|
||||||
|
{
|
||||||
|
if (IsDisposed) // lock may not be fair
|
||||||
|
return;
|
||||||
|
IsDisposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
foreach (var connection in Peers)
|
||||||
|
connection.Dispose();
|
||||||
|
Peers.Clear();
|
||||||
|
listener?.Stop();
|
||||||
|
|
||||||
|
metaFile?.Dispose();
|
||||||
|
|
||||||
|
File.Delete(MetaFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessagingServer(string editorExecutablePath, string projectMetadataDir, ILogger logger)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
|
||||||
|
MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
|
||||||
|
|
||||||
|
// Make sure the directory exists
|
||||||
|
Directory.CreateDirectory(projectMetadataDir);
|
||||||
|
|
||||||
|
// The Godot editor's file system thread can keep the file open for writing, so we are forced to allow write sharing...
|
||||||
|
const FileShare metaFileShare = FileShare.ReadWrite;
|
||||||
|
|
||||||
|
metaFile = File.Open(MetaFilePath, FileMode.Create, FileAccess.Write, metaFileShare);
|
||||||
|
|
||||||
|
listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0));
|
||||||
|
listener.Start();
|
||||||
|
|
||||||
|
int port = ((IPEndPoint)listener.Server.LocalEndPoint).Port;
|
||||||
|
using (var metaFileWriter = new StreamWriter(metaFile, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
metaFileWriter.WriteLine(port);
|
||||||
|
metaFileWriter.WriteLine(editorExecutablePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AcceptClient(TcpClient tcpClient)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Accept client...");
|
||||||
|
|
||||||
|
using (var peer = new Peer(tcpClient, new ServerHandshake(), new ServerMessageHandler(), logger))
|
||||||
|
{
|
||||||
|
// ReSharper disable AccessToDisposedClosure
|
||||||
|
peer.Connected += () =>
|
||||||
|
{
|
||||||
|
logger.LogInfo("Connection open with Ide Client");
|
||||||
|
|
||||||
|
if (clientConnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue))
|
||||||
|
{
|
||||||
|
while (queue.Count > 0)
|
||||||
|
queue.Dequeue().SetResult(true);
|
||||||
|
clientConnectedAwaiters.Remove(peer.RemoteIdentity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
peer.Disconnected += () =>
|
||||||
|
{
|
||||||
|
if (clientDisconnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue))
|
||||||
|
{
|
||||||
|
while (queue.Count > 0)
|
||||||
|
queue.Dequeue().SetResult(true);
|
||||||
|
clientDisconnectedAwaiters.Remove(peer.RemoteIdentity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// ReSharper restore AccessToDisposedClosure
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!await peer.DoHandshake("server"))
|
||||||
|
{
|
||||||
|
logger.LogError("Handshake failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError("Handshake failed with unhandled exception: ", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (await peersSem.UseAsync())
|
||||||
|
Peers.Add(peer);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await peer.Process();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
using (await peersSem.UseAsync())
|
||||||
|
Peers.Remove(peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Listen()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!IsDisposed)
|
||||||
|
_ = AcceptClient(await listener.AcceptTcpClientAsync());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted))
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void BroadcastRequest<TResponse>(string identity, Request request)
|
||||||
|
where TResponse : Response, new()
|
||||||
|
{
|
||||||
|
using (await peersSem.UseAsync())
|
||||||
|
{
|
||||||
|
if (!IsAnyConnected(identity))
|
||||||
|
{
|
||||||
|
logger.LogError("Cannot write request. No client connected to the Godot Ide Server.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedConnections = string.IsNullOrEmpty(identity) ?
|
||||||
|
Peers :
|
||||||
|
Peers.Where(c => c.RemoteIdentity == identity);
|
||||||
|
|
||||||
|
string body = JsonConvert.SerializeObject(request);
|
||||||
|
|
||||||
|
foreach (var connection in selectedConnections)
|
||||||
|
_ = connection.SendRequest<TResponse>(request.Id, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ServerHandshake : IHandshake
|
||||||
|
{
|
||||||
|
private static readonly string ServerHandshakeBase = $"{Peer.ServerHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}";
|
||||||
|
private static readonly string ClientHandshakePattern = $@"{Regex.Escape(Peer.ClientHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})";
|
||||||
|
|
||||||
|
public string GetHandshakeLine(string identity) => $"{ServerHandshakeBase},{identity}";
|
||||||
|
|
||||||
|
public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger)
|
||||||
|
{
|
||||||
|
identity = null;
|
||||||
|
|
||||||
|
var match = Regex.Match(handshake, ClientHandshakePattern);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!uint.TryParse(match.Groups[1].Value, out uint clientMajor) || Peer.ProtocolVersionMajor != clientMajor)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Incompatible major version: " + match.Groups[1].Value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||||
|
if (!uint.TryParse(match.Groups[2].Value, out uint clientMinor) || Peer.ProtocolVersionMinor > clientMinor)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ServerMessageHandler : IMessageHandler
|
||||||
|
{
|
||||||
|
private static void DispatchToMainThread(Action action)
|
||||||
|
{
|
||||||
|
var d = new SendOrPostCallback(state => action());
|
||||||
|
Godot.Dispatcher.SynchronizationContext.Post(d, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<string, Peer.RequestHandler> 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 static Dictionary<string, Peer.RequestHandler> InitializeRequestHandlers()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, Peer.RequestHandler>
|
||||||
|
{
|
||||||
|
[PlayRequest.Id] = async (peer, content) =>
|
||||||
|
{
|
||||||
|
_ = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
|
||||||
|
return await HandlePlay();
|
||||||
|
},
|
||||||
|
[DebugPlayRequest.Id] = async (peer, content) =>
|
||||||
|
{
|
||||||
|
var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
|
||||||
|
return await HandleDebugPlay(request);
|
||||||
|
},
|
||||||
|
[ReloadScriptsRequest.Id] = async (peer, content) =>
|
||||||
|
{
|
||||||
|
_ = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
|
||||||
|
return await HandleReloadScripts();
|
||||||
|
},
|
||||||
|
[CodeCompletionRequest.Id] = async (peer, content) =>
|
||||||
|
{
|
||||||
|
var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
|
||||||
|
return await HandleCodeCompletionRequest(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<Response> HandlePlay()
|
||||||
|
{
|
||||||
|
DispatchToMainThread(() =>
|
||||||
|
{
|
||||||
|
GodotSharpEditor.Instance.CurrentPlaySettings = new PlaySettings();
|
||||||
|
Internal.EditorRunPlay();
|
||||||
|
GodotSharpEditor.Instance.CurrentPlaySettings = null;
|
||||||
|
});
|
||||||
|
return Task.FromResult<Response>(new PlayResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<Response> HandleDebugPlay(DebugPlayRequest request)
|
||||||
|
{
|
||||||
|
DispatchToMainThread(() =>
|
||||||
|
{
|
||||||
|
GodotSharpEditor.Instance.CurrentPlaySettings =
|
||||||
|
new PlaySettings(request.DebuggerHost, request.DebuggerPort, buildBeforePlaying: true);
|
||||||
|
Internal.EditorRunPlay();
|
||||||
|
GodotSharpEditor.Instance.CurrentPlaySettings = null;
|
||||||
|
});
|
||||||
|
return Task.FromResult<Response>(new DebugPlayResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<Response> HandleReloadScripts()
|
||||||
|
{
|
||||||
|
DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts);
|
||||||
|
return Task.FromResult<Response>(new ReloadScriptsResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Response> HandleCodeCompletionRequest(CodeCompletionRequest request)
|
||||||
|
{
|
||||||
|
var response = new CodeCompletionResponse {Kind = request.Kind, ScriptFile = request.ScriptFile};
|
||||||
|
response.Suggestions = await Task.Run(() => Internal.CodeCompletionRequest(response.Kind, response.ScriptFile));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,14 +7,16 @@ using GodotTools.Utils;
|
||||||
|
|
||||||
namespace GodotTools.Ides.MonoDevelop
|
namespace GodotTools.Ides.MonoDevelop
|
||||||
{
|
{
|
||||||
public class Instance
|
public class Instance : IDisposable
|
||||||
{
|
{
|
||||||
|
public DateTime LaunchTime { get; private set; }
|
||||||
private readonly string solutionFile;
|
private readonly string solutionFile;
|
||||||
private readonly EditorId editorId;
|
private readonly EditorId editorId;
|
||||||
|
|
||||||
private Process process;
|
private Process process;
|
||||||
|
|
||||||
public bool IsRunning => process != null && !process.HasExited;
|
public bool IsRunning => process != null && !process.HasExited;
|
||||||
|
public bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
public void Execute()
|
public void Execute()
|
||||||
{
|
{
|
||||||
|
@ -59,6 +61,8 @@ namespace GodotTools.Ides.MonoDevelop
|
||||||
if (command == null)
|
if (command == null)
|
||||||
throw new FileNotFoundException();
|
throw new FileNotFoundException();
|
||||||
|
|
||||||
|
LaunchTime = DateTime.Now;
|
||||||
|
|
||||||
if (newWindow)
|
if (newWindow)
|
||||||
{
|
{
|
||||||
process = Process.Start(new ProcessStartInfo
|
process = Process.Start(new ProcessStartInfo
|
||||||
|
@ -88,6 +92,12 @@ namespace GodotTools.Ides.MonoDevelop
|
||||||
this.editorId = editorId;
|
this.editorId = editorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
IsDisposed = true;
|
||||||
|
process?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames;
|
private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames;
|
||||||
private static readonly IReadOnlyDictionary<EditorId, string> BundleIds;
|
private static readonly IReadOnlyDictionary<EditorId, string> BundleIds;
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Godot;
|
using Godot;
|
||||||
using Godot.Collections;
|
using Godot.Collections;
|
||||||
|
using GodotTools.IdeMessaging.Requests;
|
||||||
|
|
||||||
namespace GodotTools.Internals
|
namespace GodotTools.Internals
|
||||||
{
|
{
|
||||||
|
@ -52,6 +53,9 @@ namespace GodotTools.Internals
|
||||||
|
|
||||||
public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts();
|
public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts();
|
||||||
|
|
||||||
|
public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) =>
|
||||||
|
internal_CodeCompletionRequest((int)kind, scriptFile);
|
||||||
|
|
||||||
#region Internal
|
#region Internal
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||||
|
@ -111,6 +115,9 @@ namespace GodotTools.Internals
|
||||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||||
private static extern void internal_ScriptEditorDebugger_ReloadScripts();
|
private static extern void internal_ScriptEditorDebugger_ReloadScripts();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||||
|
private static extern string[] internal_CodeCompletionRequest(int kind, string scriptFile);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
namespace GodotTools
|
||||||
|
{
|
||||||
|
public struct PlaySettings
|
||||||
|
{
|
||||||
|
public bool HasDebugger { get; }
|
||||||
|
public string DebuggerHost { get; }
|
||||||
|
public int DebuggerPort { get; }
|
||||||
|
|
||||||
|
public bool BuildBeforePlaying { get; }
|
||||||
|
|
||||||
|
public PlaySettings(string debuggerHost, int debuggerPort, bool buildBeforePlaying)
|
||||||
|
{
|
||||||
|
HasDebugger = true;
|
||||||
|
DebuggerHost = debuggerHost;
|
||||||
|
DebuggerPort = debuggerPort;
|
||||||
|
BuildBeforePlaying = buildBeforePlaying;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
|
<package id="GodotTools.IdeMessaging" version="1.1.0" targetFramework="net47" />
|
||||||
<package id="JetBrains.Annotations" version="2019.1.3" targetFramework="net45" />
|
<package id="JetBrains.Annotations" version="2019.1.3" targetFramework="net45" />
|
||||||
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net45" />
|
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net45" />
|
||||||
</packages>
|
</packages>
|
|
@ -0,0 +1,249 @@
|
||||||
|
/*************************************************************************/
|
||||||
|
/* code_completion.cpp */
|
||||||
|
/*************************************************************************/
|
||||||
|
/* This file is part of: */
|
||||||
|
/* GODOT ENGINE */
|
||||||
|
/* https://godotengine.org */
|
||||||
|
/*************************************************************************/
|
||||||
|
/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
|
||||||
|
/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
|
||||||
|
/* */
|
||||||
|
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||||
|
/* a copy of this software and associated documentation files (the */
|
||||||
|
/* "Software"), to deal in the Software without restriction, including */
|
||||||
|
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||||
|
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||||
|
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||||
|
/* the following conditions: */
|
||||||
|
/* */
|
||||||
|
/* The above copyright notice and this permission notice shall be */
|
||||||
|
/* included in all copies or substantial portions of the Software. */
|
||||||
|
/* */
|
||||||
|
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||||
|
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||||
|
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
|
||||||
|
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||||
|
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||||
|
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||||
|
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||||
|
/*************************************************************************/
|
||||||
|
|
||||||
|
#include "code_completion.h"
|
||||||
|
|
||||||
|
#include "core/project_settings.h"
|
||||||
|
#include "editor/editor_file_system.h"
|
||||||
|
#include "editor/editor_settings.h"
|
||||||
|
#include "scene/gui/control.h"
|
||||||
|
#include "scene/main/node.h"
|
||||||
|
|
||||||
|
namespace gdmono {
|
||||||
|
|
||||||
|
// Almost everything here is taken from functions used by GDScript for code completion, adapted for C#.
|
||||||
|
|
||||||
|
_FORCE_INLINE_ String quoted(const String &p_str) {
|
||||||
|
return "\"" + p_str + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
void _add_nodes_suggestions(const Node *p_base, const Node *p_node, PoolStringArray &r_suggestions) {
|
||||||
|
if (p_node != p_base && !p_node->get_owner())
|
||||||
|
return;
|
||||||
|
|
||||||
|
String path_relative_to_orig = p_base->get_path_to(p_node);
|
||||||
|
|
||||||
|
r_suggestions.push_back(quoted(path_relative_to_orig));
|
||||||
|
|
||||||
|
for (int i = 0; i < p_node->get_child_count(); i++) {
|
||||||
|
_add_nodes_suggestions(p_base, p_node->get_child(i), r_suggestions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Node *_find_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
|
||||||
|
if (p_current->get_owner() != p_base && p_base != p_current)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
Ref<Script> c = p_current->get_script();
|
||||||
|
|
||||||
|
if (c == p_script)
|
||||||
|
return p_current;
|
||||||
|
|
||||||
|
for (int i = 0; i < p_current->get_child_count(); i++) {
|
||||||
|
Node *found = _find_node_for_script(p_base, p_current->get_child(i), p_script);
|
||||||
|
if (found)
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _get_directory_contents(EditorFileSystemDirectory *p_dir, PoolStringArray &r_suggestions) {
|
||||||
|
for (int i = 0; i < p_dir->get_file_count(); i++) {
|
||||||
|
r_suggestions.push_back(quoted(p_dir->get_file_path(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < p_dir->get_subdir_count(); i++) {
|
||||||
|
_get_directory_contents(p_dir->get_subdir(i), r_suggestions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Node *_try_find_owner_node_in_tree(const Ref<Script> p_script) {
|
||||||
|
SceneTree *tree = SceneTree::get_singleton();
|
||||||
|
if (!tree)
|
||||||
|
return nullptr;
|
||||||
|
Node *base = tree->get_edited_scene_root();
|
||||||
|
if (base) {
|
||||||
|
base = _find_node_for_script(base, base, p_script);
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
PoolStringArray get_code_completion(CompletionKind p_kind, const String &p_script_file) {
|
||||||
|
PoolStringArray suggestions;
|
||||||
|
|
||||||
|
switch (p_kind) {
|
||||||
|
case CompletionKind::INPUT_ACTIONS: {
|
||||||
|
List<PropertyInfo> project_props;
|
||||||
|
ProjectSettings::get_singleton()->get_property_list(&project_props);
|
||||||
|
|
||||||
|
for (List<PropertyInfo>::Element *E = project_props.front(); E; E = E->next()) {
|
||||||
|
const PropertyInfo &prop = E->get();
|
||||||
|
|
||||||
|
if (!prop.name.begins_with("input/"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
String name = prop.name.substr(prop.name.find("/") + 1, prop.name.length());
|
||||||
|
suggestions.push_back(quoted(name));
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case CompletionKind::NODE_PATHS: {
|
||||||
|
{
|
||||||
|
// AutoLoads
|
||||||
|
List<PropertyInfo> props;
|
||||||
|
ProjectSettings::get_singleton()->get_property_list(&props);
|
||||||
|
|
||||||
|
for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) {
|
||||||
|
String s = E->get().name;
|
||||||
|
if (!s.begins_with("autoload/")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = s.get_slice("/", 1);
|
||||||
|
suggestions.push_back(quoted("/root/" + name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Current edited scene tree
|
||||||
|
Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
|
||||||
|
Node *base = _try_find_owner_node_in_tree(script);
|
||||||
|
if (base) {
|
||||||
|
_add_nodes_suggestions(base, base, suggestions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case CompletionKind::RESOURCE_PATHS: {
|
||||||
|
if (bool(EditorSettings::get_singleton()->get("text_editor/completion/complete_file_paths"))) {
|
||||||
|
_get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), suggestions);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case CompletionKind::SCENE_PATHS: {
|
||||||
|
DirAccessRef dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES);
|
||||||
|
List<String> directories;
|
||||||
|
directories.push_back(dir_access->get_current_dir());
|
||||||
|
|
||||||
|
while (!directories.empty()) {
|
||||||
|
dir_access->change_dir(directories.back()->get());
|
||||||
|
directories.pop_back();
|
||||||
|
|
||||||
|
dir_access->list_dir_begin();
|
||||||
|
String filename = dir_access->get_next();
|
||||||
|
|
||||||
|
while (filename != "") {
|
||||||
|
if (filename == "." || filename == "..") {
|
||||||
|
filename = dir_access->get_next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dir_access->dir_exists(filename)) {
|
||||||
|
directories.push_back(dir_access->get_current_dir().plus_file(filename));
|
||||||
|
} else if (filename.ends_with(".tscn") || filename.ends_with(".scn")) {
|
||||||
|
suggestions.push_back(quoted(dir_access->get_current_dir().plus_file(filename)));
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = dir_access->get_next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case CompletionKind::SHADER_PARAMS: {
|
||||||
|
print_verbose("Shared params completion for C# not implemented.");
|
||||||
|
} break;
|
||||||
|
case CompletionKind::SIGNALS: {
|
||||||
|
Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
|
||||||
|
|
||||||
|
List<MethodInfo> signals;
|
||||||
|
script->get_script_signal_list(&signals);
|
||||||
|
|
||||||
|
StringName native = script->get_instance_base_type();
|
||||||
|
if (native != StringName()) {
|
||||||
|
ClassDB::get_signal_list(native, &signals, /* p_no_inheritance: */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (List<MethodInfo>::Element *E = signals.front(); E; E = E->next()) {
|
||||||
|
const String &signal = E->get().name;
|
||||||
|
suggestions.push_back(quoted(signal));
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case CompletionKind::THEME_COLORS: {
|
||||||
|
Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
|
||||||
|
Node *base = _try_find_owner_node_in_tree(script);
|
||||||
|
if (base && Object::cast_to<Control>(base)) {
|
||||||
|
List<StringName> sn;
|
||||||
|
Theme::get_default()->get_color_list(base->get_class(), &sn);
|
||||||
|
|
||||||
|
for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
|
||||||
|
suggestions.push_back(quoted(E->get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case CompletionKind::THEME_CONSTANTS: {
|
||||||
|
Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
|
||||||
|
Node *base = _try_find_owner_node_in_tree(script);
|
||||||
|
if (base && Object::cast_to<Control>(base)) {
|
||||||
|
List<StringName> sn;
|
||||||
|
Theme::get_default()->get_constant_list(base->get_class(), &sn);
|
||||||
|
|
||||||
|
for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
|
||||||
|
suggestions.push_back(quoted(E->get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case CompletionKind::THEME_FONTS: {
|
||||||
|
Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
|
||||||
|
Node *base = _try_find_owner_node_in_tree(script);
|
||||||
|
if (base && Object::cast_to<Control>(base)) {
|
||||||
|
List<StringName> sn;
|
||||||
|
Theme::get_default()->get_font_list(base->get_class(), &sn);
|
||||||
|
|
||||||
|
for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
|
||||||
|
suggestions.push_back(quoted(E->get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case CompletionKind::THEME_STYLES: {
|
||||||
|
Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
|
||||||
|
Node *base = _try_find_owner_node_in_tree(script);
|
||||||
|
if (base && Object::cast_to<Control>(base)) {
|
||||||
|
List<StringName> sn;
|
||||||
|
Theme::get_default()->get_stylebox_list(base->get_class(), &sn);
|
||||||
|
|
||||||
|
for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
|
||||||
|
suggestions.push_back(quoted(E->get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
default:
|
||||||
|
ERR_FAIL_V_MSG(suggestions, "Invalid completion kind.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace gdmono
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*************************************************************************/
|
||||||
|
/* code_completion.h */
|
||||||
|
/*************************************************************************/
|
||||||
|
/* This file is part of: */
|
||||||
|
/* GODOT ENGINE */
|
||||||
|
/* https://godotengine.org */
|
||||||
|
/*************************************************************************/
|
||||||
|
/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
|
||||||
|
/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
|
||||||
|
/* */
|
||||||
|
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||||
|
/* a copy of this software and associated documentation files (the */
|
||||||
|
/* "Software"), to deal in the Software without restriction, including */
|
||||||
|
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||||
|
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||||
|
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||||
|
/* the following conditions: */
|
||||||
|
/* */
|
||||||
|
/* The above copyright notice and this permission notice shall be */
|
||||||
|
/* included in all copies or substantial portions of the Software. */
|
||||||
|
/* */
|
||||||
|
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||||
|
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||||
|
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
|
||||||
|
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||||
|
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||||
|
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||||
|
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||||
|
/*************************************************************************/
|
||||||
|
|
||||||
|
#ifndef CODE_COMPLETION_H
|
||||||
|
#define CODE_COMPLETION_H
|
||||||
|
|
||||||
|
#include "core/ustring.h"
|
||||||
|
#include "core/variant.h"
|
||||||
|
|
||||||
|
namespace gdmono {
|
||||||
|
|
||||||
|
enum class CompletionKind {
|
||||||
|
INPUT_ACTIONS = 0,
|
||||||
|
NODE_PATHS,
|
||||||
|
RESOURCE_PATHS,
|
||||||
|
SCENE_PATHS,
|
||||||
|
SHADER_PARAMS,
|
||||||
|
SIGNALS,
|
||||||
|
THEME_COLORS,
|
||||||
|
THEME_CONSTANTS,
|
||||||
|
THEME_FONTS,
|
||||||
|
THEME_STYLES
|
||||||
|
};
|
||||||
|
|
||||||
|
PoolStringArray get_code_completion(CompletionKind p_kind, const String &p_script_file);
|
||||||
|
|
||||||
|
} // namespace gdmono
|
||||||
|
|
||||||
|
#endif // CODE_COMPLETION_H
|
|
@ -48,6 +48,7 @@
|
||||||
#include "../mono_gd/gd_mono_marshal.h"
|
#include "../mono_gd/gd_mono_marshal.h"
|
||||||
#include "../utils/osx_utils.h"
|
#include "../utils/osx_utils.h"
|
||||||
#include "bindings_generator.h"
|
#include "bindings_generator.h"
|
||||||
|
#include "code_completion.h"
|
||||||
#include "godotsharp_export.h"
|
#include "godotsharp_export.h"
|
||||||
#include "script_class_parser.h"
|
#include "script_class_parser.h"
|
||||||
|
|
||||||
|
@ -354,6 +355,12 @@ void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MonoArray *godot_icall_Internal_CodeCompletionRequest(int32_t p_kind, MonoString *p_script_file) {
|
||||||
|
String script_file = GDMonoMarshal::mono_string_to_godot(p_script_file);
|
||||||
|
PoolStringArray suggestions = gdmono::get_code_completion((gdmono::CompletionKind)p_kind, script_file);
|
||||||
|
return GDMonoMarshal::PoolStringArray_to_mono_array(suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
float godot_icall_Globals_EditorScale() {
|
float godot_icall_Globals_EditorScale() {
|
||||||
return EDSCALE;
|
return EDSCALE;
|
||||||
}
|
}
|
||||||
|
@ -454,6 +461,7 @@ void register_editor_internal_calls() {
|
||||||
mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunPlay", (void *)godot_icall_Internal_EditorRunPlay);
|
mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunPlay", (void *)godot_icall_Internal_EditorRunPlay);
|
||||||
mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunStop", (void *)godot_icall_Internal_EditorRunStop);
|
mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunStop", (void *)godot_icall_Internal_EditorRunStop);
|
||||||
mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorDebugger_ReloadScripts", (void *)godot_icall_Internal_ScriptEditorDebugger_ReloadScripts);
|
mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorDebugger_ReloadScripts", (void *)godot_icall_Internal_ScriptEditorDebugger_ReloadScripts);
|
||||||
|
mono_add_internal_call("GodotTools.Internals.Internal::internal_CodeCompletionRequest", (void *)godot_icall_Internal_CodeCompletionRequest);
|
||||||
|
|
||||||
// Globals
|
// Globals
|
||||||
mono_add_internal_call("GodotTools.Internals.Globals::internal_EditorScale", (void *)godot_icall_Globals_EditorScale);
|
mono_add_internal_call("GodotTools.Internals.Globals::internal_EditorScale", (void *)godot_icall_Globals_EditorScale);
|
||||||
|
|
|
@ -132,6 +132,10 @@ void gd_mono_debug_init() {
|
||||||
|
|
||||||
CharString da_args = OS::get_singleton()->get_environment("GODOT_MONO_DEBUGGER_AGENT").utf8();
|
CharString da_args = OS::get_singleton()->get_environment("GODOT_MONO_DEBUGGER_AGENT").utf8();
|
||||||
|
|
||||||
|
if (da_args.length()) {
|
||||||
|
OS::get_singleton()->set_environment("GODOT_MONO_DEBUGGER_AGENT", String());
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef TOOLS_ENABLED
|
#ifdef TOOLS_ENABLED
|
||||||
int da_port = GLOBAL_DEF("mono/debugger_agent/port", 23685);
|
int da_port = GLOBAL_DEF("mono/debugger_agent/port", 23685);
|
||||||
bool da_suspend = GLOBAL_DEF("mono/debugger_agent/wait_for_debugger", false);
|
bool da_suspend = GLOBAL_DEF("mono/debugger_agent/wait_for_debugger", false);
|
||||||
|
|
Loading…
Reference in New Issue