diff options
author | Ignacio Etcheverry <ignalfonsore@gmail.com> | 2019-07-18 04:08:24 +0200 |
---|---|---|
committer | Ignacio Etcheverry <ignalfonsore@gmail.com> | 2019-08-04 01:57:53 +0200 |
commit | 0b94203a79d3261d4cc3bbcdb3438a5a45c8c572 (patch) | |
tree | d36bb081a3bda748a55a996578efb650d78a06b2 /modules | |
parent | 4b7b1b0d4acf8d49505a839a0aa745ce60641545 (diff) |
C#: Add Ide Connection library and server for the editor
This will be used for communicating between the Godot editor and external IDEs/editors, for things like opening files, triggering hot-reload and running the game with a debugger attached.
Diffstat (limited to 'modules')
42 files changed, 1624 insertions, 239 deletions
diff --git a/modules/mono/build_scripts/godot_tools_build.py b/modules/mono/build_scripts/godot_tools_build.py index c47cfc8a38..35daa6d307 100644 --- a/modules/mono/build_scripts/godot_tools_build.py +++ b/modules/mono/build_scripts/godot_tools_build.py @@ -84,10 +84,16 @@ def build(env_mono): source_filenames = ['GodotSharp.dll', 'GodotSharpEditor.dll'] sources = [os.path.join(editor_api_dir, filename) for filename in source_filenames] - target_filenames = ['GodotTools.dll', 'GodotTools.BuildLogger.dll', 'GodotTools.ProjectEditor.dll', 'DotNet.Glob.dll', 'GodotTools.Core.dll'] + target_filenames = [ + 'GodotTools.dll', 'GodotTools.IdeConnection.dll', 'GodotTools.BuildLogger.dll', + 'GodotTools.ProjectEditor.dll', 'DotNet.Glob.dll', 'GodotTools.Core.dll' + ] if env_mono['target'] == 'debug': - target_filenames += ['GodotTools.pdb', 'GodotTools.BuildLogger.pdb', 'GodotTools.ProjectEditor.pdb', 'GodotTools.Core.pdb'] + target_filenames += [ + 'GodotTools.pdb', 'GodotTools.IdeConnection.pdb', 'GodotTools.BuildLogger.pdb', + 'GodotTools.ProjectEditor.pdb', 'GodotTools.Core.pdb' + ] targets = [os.path.join(editor_tools_dir, filename) for filename in target_filenames] diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs new file mode 100644 index 0000000000..7a2ff2ca56 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs @@ -0,0 +1,33 @@ +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); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs new file mode 100644 index 0000000000..be89638241 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs @@ -0,0 +1,94 @@ +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(); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs new file mode 100644 index 0000000000..4f56a8d71b --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs @@ -0,0 +1,219 @@ +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); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs new file mode 100644 index 0000000000..e7e81f175e --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs @@ -0,0 +1,207 @@ +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(); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs new file mode 100644 index 0000000000..1b11a14358 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs @@ -0,0 +1,24 @@ +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; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs new file mode 100644 index 0000000000..aa98dc7ca3 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs @@ -0,0 +1,24 @@ +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; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs new file mode 100644 index 0000000000..d16daba0e2 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs @@ -0,0 +1,45 @@ +namespace GodotTools.IdeConnection +{ + public struct GodotIdeMetadata + { + public int Port { get; } + public string EditorExecutablePath { get; } + + public GodotIdeMetadata(int port, string editorExecutablePath) + { + Port = port; + EditorExecutablePath = editorExecutablePath; + } + + public static bool operator ==(GodotIdeMetadata a, GodotIdeMetadata b) + { + return a.Port == b.Port && a.EditorExecutablePath == b.EditorExecutablePath; + } + + public static bool operator !=(GodotIdeMetadata a, GodotIdeMetadata b) + { + return !(a == b); + } + + public override bool Equals(object obj) + { + if (obj is GodotIdeMetadata metadata) + return metadata == this; + + return false; + } + + public bool Equals(GodotIdeMetadata other) + { + return Port == other.Port && EditorExecutablePath == other.EditorExecutablePath; + } + + public override int GetHashCode() + { + unchecked + { + return (Port * 397) ^ (EditorExecutablePath != null ? EditorExecutablePath.GetHashCode() : 0); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj new file mode 100644 index 0000000000..84c08251ab --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj @@ -0,0 +1,52 @@ +<?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.5</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + </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>
\ No newline at end of file diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs new file mode 100644 index 0000000000..614bb30271 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs @@ -0,0 +1,13 @@ +using System; + +namespace GodotTools.IdeConnection +{ + public interface ILogger + { + void LogDebug(string message); + void LogInfo(string message); + void LogWarning(string message); + void LogError(string message); + void LogError(string message, Exception e); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs new file mode 100644 index 0000000000..f24d324ae3 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs @@ -0,0 +1,21 @@ +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)}')"; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs new file mode 100644 index 0000000000..9e4cd6ec1a --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs @@ -0,0 +1,46 @@ +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(); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs new file mode 100644 index 0000000000..ed691e481f --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs @@ -0,0 +1,88 @@ +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; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..c7c00e66a2 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +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")] diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index 6f7d44bec2..a3438ea5f3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Core", "GodotToo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.BuildLogger", "GodotTools.BuildLogger\GodotTools.BuildLogger.csproj", "{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.IdeConnection", "GodotTools.IdeConnection\GodotTools.IdeConnection.csproj", "{92600954-25F0-4291-8E11-1FEE9FC4BE20}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,5 +33,9 @@ Global {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.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 EndGlobal diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoBottomPanel.cs b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs index 53ff0891d5..44813f962c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/MonoBottomPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs @@ -9,7 +9,7 @@ using Path = System.IO.Path; namespace GodotTools { - public class MonoBottomPanel : VBoxContainer + public class BottomPanel : VBoxContainer { private EditorInterface editorInterface; @@ -34,7 +34,7 @@ namespace GodotTools for (int i = 0; i < buildTabs.GetChildCount(); i++) { - var tab = (MonoBuildTab) buildTabs.GetChild(i); + var tab = (BuildTab) buildTabs.GetChild(i); if (tab == null) continue; @@ -49,11 +49,11 @@ namespace GodotTools itemTooltip += "\nStatus: "; if (tab.BuildExited) - itemTooltip += tab.BuildResult == MonoBuildTab.BuildResults.Success ? "Succeeded" : "Errored"; + itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored"; else itemTooltip += "Running"; - if (!tab.BuildExited || tab.BuildResult == MonoBuildTab.BuildResults.Error) + if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error) itemTooltip += $"\nErrors: {tab.ErrorCount}"; itemTooltip += $"\nWarnings: {tab.WarningCount}"; @@ -68,15 +68,15 @@ namespace GodotTools } } - public MonoBuildTab GetBuildTabFor(MonoBuildInfo buildInfo) + public BuildTab GetBuildTabFor(BuildInfo buildInfo) { - foreach (var buildTab in new Array<MonoBuildTab>(buildTabs.GetChildren())) + foreach (var buildTab in new Array<BuildTab>(buildTabs.GetChildren())) { if (buildTab.BuildInfo.Equals(buildInfo)) return buildTab; } - var newBuildTab = new MonoBuildTab(buildInfo); + var newBuildTab = new BuildTab(buildInfo); AddBuildTab(newBuildTab); return newBuildTab; @@ -120,7 +120,7 @@ namespace GodotTools if (currentTab < 0 || currentTab >= buildTabs.GetTabCount()) throw new InvalidOperationException("No tab selected"); - var buildTab = (MonoBuildTab) buildTabs.GetChild(currentTab); + var buildTab = (BuildTab) buildTabs.GetChild(currentTab); buildTab.WarningsVisible = pressed; buildTab.UpdateIssuesList(); } @@ -132,7 +132,7 @@ namespace GodotTools if (currentTab < 0 || currentTab >= buildTabs.GetTabCount()) throw new InvalidOperationException("No tab selected"); - var buildTab = (MonoBuildTab) buildTabs.GetChild(currentTab); + var buildTab = (BuildTab) buildTabs.GetChild(currentTab); buildTab.ErrorsVisible = pressed; buildTab.UpdateIssuesList(); } @@ -145,7 +145,7 @@ namespace GodotTools string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor"); string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player"); - CSharpProject.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath); + CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath); if (File.Exists(editorScriptsMetadataPath)) { @@ -166,7 +166,7 @@ namespace GodotTools Internal.GodotIs32Bits() ? "32" : "64" }; - bool buildSuccess = GodotSharpBuilds.BuildProjectBlocking("Tools", godotDefines); + bool buildSuccess = BuildManager.BuildProjectBlocking("Tools", godotDefines); if (!buildSuccess) return; @@ -193,9 +193,9 @@ namespace GodotTools int selectedItem = selectedItems[0]; - var buildTab = (MonoBuildTab) buildTabs.GetTabControl(selectedItem); + var buildTab = (BuildTab) buildTabs.GetTabControl(selectedItem); - OS.ShellOpen(Path.Combine(buildTab.BuildInfo.LogsDirPath, GodotSharpBuilds.MsBuildLogFileName)); + OS.ShellOpen(Path.Combine(buildTab.BuildInfo.LogsDirPath, BuildManager.MsBuildLogFileName)); } public override void _Notification(int what) @@ -211,13 +211,13 @@ namespace GodotTools } } - public void AddBuildTab(MonoBuildTab buildTab) + public void AddBuildTab(BuildTab buildTab) { buildTabs.AddChild(buildTab); RaiseBuildTab(buildTab); } - public void RaiseBuildTab(MonoBuildTab buildTab) + public void RaiseBuildTab(BuildTab buildTab) { if (buildTab.GetParent() != buildTabs) throw new InvalidOperationException("Build tab is not in the tabs list"); diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index d8cb9024c3..9a2b2e3a26 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -46,8 +46,8 @@ namespace GodotTools.Build { if (OS.IsWindows()) { - return (GodotSharpBuilds.BuildTool) EditorSettings.GetSetting("mono/builds/build_tool") - == GodotSharpBuilds.BuildTool.MsBuildMono; + return (BuildManager.BuildTool) EditorSettings.GetSetting("mono/builds/build_tool") + == BuildManager.BuildTool.MsBuildMono; } return false; @@ -103,16 +103,16 @@ namespace GodotTools.Build return process; } - public static int Build(MonoBuildInfo monoBuildInfo) + public static int Build(BuildInfo buildInfo) { - return Build(monoBuildInfo.Solution, monoBuildInfo.Configuration, - monoBuildInfo.LogsDirPath, monoBuildInfo.CustomProperties); + return Build(buildInfo.Solution, buildInfo.Configuration, + buildInfo.LogsDirPath, buildInfo.CustomProperties); } - public static async Task<int> BuildAsync(MonoBuildInfo monoBuildInfo) + public static async Task<int> BuildAsync(BuildInfo buildInfo) { - return await BuildAsync(monoBuildInfo.Solution, monoBuildInfo.Configuration, - monoBuildInfo.LogsDirPath, monoBuildInfo.CustomProperties); + return await BuildAsync(buildInfo.Solution, buildInfo.Configuration, + buildInfo.LogsDirPath, buildInfo.CustomProperties); } public static int Build(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs index 926aabdf89..4c1e47ecad 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs @@ -19,13 +19,13 @@ namespace GodotTools.Build public static string FindMsBuild() { var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (GodotSharpBuilds.BuildTool) editorSettings.GetSetting("mono/builds/build_tool"); + var buildTool = (BuildManager.BuildTool) editorSettings.GetSetting("mono/builds/build_tool"); if (OS.IsWindows()) { switch (buildTool) { - case GodotSharpBuilds.BuildTool.MsBuildVs: + case BuildManager.BuildTool.MsBuildVs: { if (_msbuildToolsPath.Empty() || !File.Exists(_msbuildToolsPath)) { @@ -34,7 +34,7 @@ namespace GodotTools.Build if (_msbuildToolsPath.Empty()) { - throw new FileNotFoundException($"Cannot find executable for '{GodotSharpBuilds.PropNameMsbuildVs}'. Tried with path: {_msbuildToolsPath}"); + throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildVs}'. Tried with path: {_msbuildToolsPath}"); } } @@ -43,13 +43,13 @@ namespace GodotTools.Build return Path.Combine(_msbuildToolsPath, "MSBuild.exe"); } - case GodotSharpBuilds.BuildTool.MsBuildMono: + case BuildManager.BuildTool.MsBuildMono: { string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat"); if (!File.Exists(msbuildPath)) { - throw new FileNotFoundException($"Cannot find executable for '{GodotSharpBuilds.PropNameMsbuildMono}'. Tried with path: {msbuildPath}"); + throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildMono}'. Tried with path: {msbuildPath}"); } return msbuildPath; @@ -61,7 +61,7 @@ namespace GodotTools.Build if (OS.IsUnix()) { - if (buildTool == GodotSharpBuilds.BuildTool.MsBuildMono) + if (buildTool == BuildManager.BuildTool.MsBuildMono) { if (_msbuildUnixPath.Empty() || !File.Exists(_msbuildUnixPath)) { @@ -71,7 +71,7 @@ namespace GodotTools.Build if (_msbuildUnixPath.Empty()) { - throw new FileNotFoundException($"Cannot find binary for '{GodotSharpBuilds.PropNameMsbuildMono}'"); + throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMsbuildMono}'"); } return _msbuildUnixPath; diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoBuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs index 858e852392..70bd552f2f 100644 --- a/modules/mono/editor/GodotTools/GodotTools/MonoBuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs @@ -7,7 +7,7 @@ using Path = System.IO.Path; namespace GodotTools { [Serializable] - public sealed class MonoBuildInfo : Reference // TODO Remove Reference once we have proper serialization + public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization { public string Solution { get; } public string Configuration { get; } @@ -17,7 +17,7 @@ namespace GodotTools public override bool Equals(object obj) { - if (obj is MonoBuildInfo other) + if (obj is BuildInfo other) return other.Solution == Solution && other.Configuration == Configuration; return false; @@ -34,11 +34,11 @@ namespace GodotTools } } - private MonoBuildInfo() + private BuildInfo() { } - public MonoBuildInfo(string solution, string configuration) + public BuildInfo(string solution, string configuration) { Solution = solution; Configuration = configuration; diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs index de3a4d9a6e..417032da54 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs @@ -6,15 +6,13 @@ using GodotTools.Build; using GodotTools.Internals; using GodotTools.Utils; using static GodotTools.Internals.Globals; -using Error = Godot.Error; using File = GodotTools.Utils.File; -using Directory = GodotTools.Utils.Directory; namespace GodotTools { - public static class GodotSharpBuilds + public static class BuildManager { - private static readonly List<MonoBuildInfo> BuildsInProgress = new List<MonoBuildInfo>(); + private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>(); public const string PropNameMsbuildMono = "MSBuild (Mono)"; public const string PropNameMsbuildVs = "MSBuild (VS Build Tools)"; @@ -28,7 +26,7 @@ namespace GodotTools MsBuildVs } - private static void RemoveOldIssuesFile(MonoBuildInfo buildInfo) + private static void RemoveOldIssuesFile(BuildInfo buildInfo) { var issuesFile = GetIssuesFilePath(buildInfo); @@ -38,29 +36,21 @@ namespace GodotTools File.Delete(issuesFile); } - private static string _ApiFolderName(ApiAssemblyType apiType) - { - ulong apiHash = apiType == ApiAssemblyType.Core ? - Internal.GetCoreApiHash() : - Internal.GetEditorApiHash(); - return $"{apiHash}_{BindingsGenerator.Version}_{BindingsGenerator.CsGlueVersion}"; - } - private static void ShowBuildErrorDialog(string message) { GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error"); - GodotSharpEditor.Instance.MonoBottomPanel.ShowBuildTab(); + GodotSharpEditor.Instance.BottomPanel.ShowBuildTab(); } - public static void RestartBuild(MonoBuildTab buildTab) => throw new NotImplementedException(); - public static void StopBuild(MonoBuildTab buildTab) => throw new NotImplementedException(); + public static void RestartBuild(BuildTab buildTab) => throw new NotImplementedException(); + public static void StopBuild(BuildTab buildTab) => throw new NotImplementedException(); - private static string GetLogFilePath(MonoBuildInfo buildInfo) + private static string GetLogFilePath(BuildInfo buildInfo) { return Path.Combine(buildInfo.LogsDirPath, MsBuildLogFileName); } - private static string GetIssuesFilePath(MonoBuildInfo buildInfo) + private static string GetIssuesFilePath(BuildInfo buildInfo) { return Path.Combine(buildInfo.LogsDirPath, MsBuildIssuesFileName); } @@ -71,7 +61,7 @@ namespace GodotTools Godot.GD.Print(text); } - public static bool Build(MonoBuildInfo buildInfo) + public static bool Build(BuildInfo buildInfo) { if (BuildsInProgress.Contains(buildInfo)) throw new InvalidOperationException("A build is already in progress"); @@ -80,7 +70,7 @@ namespace GodotTools try { - MonoBuildTab buildTab = GodotSharpEditor.Instance.MonoBottomPanel.GetBuildTabFor(buildInfo); + BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo); buildTab.OnBuildStart(); // Required in order to update the build tasks list @@ -103,7 +93,7 @@ namespace GodotTools if (exitCode != 0) PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); - buildTab.OnBuildExit(exitCode == 0 ? MonoBuildTab.BuildResults.Success : MonoBuildTab.BuildResults.Error); + buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error); return exitCode == 0; } @@ -120,7 +110,7 @@ namespace GodotTools } } - public static async Task<bool> BuildAsync(MonoBuildInfo buildInfo) + public static async Task<bool> BuildAsync(BuildInfo buildInfo) { if (BuildsInProgress.Contains(buildInfo)) throw new InvalidOperationException("A build is already in progress"); @@ -129,7 +119,7 @@ namespace GodotTools try { - MonoBuildTab buildTab = GodotSharpEditor.Instance.MonoBottomPanel.GetBuildTabFor(buildInfo); + BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo); try { @@ -148,7 +138,7 @@ namespace GodotTools if (exitCode != 0) PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); - buildTab.OnBuildExit(exitCode == 0 ? MonoBuildTab.BuildResults.Success : MonoBuildTab.BuildResults.Error); + buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error); return exitCode == 0; } @@ -165,32 +155,6 @@ namespace GodotTools } } - public static bool BuildApiSolution(string apiSlnDir, string config) - { - string apiSlnFile = Path.Combine(apiSlnDir, $"{ApiAssemblyNames.SolutionName}.sln"); - - string coreApiAssemblyDir = Path.Combine(apiSlnDir, ApiAssemblyNames.Core, "bin", config); - string coreApiAssemblyFile = Path.Combine(coreApiAssemblyDir, $"{ApiAssemblyNames.Core}.dll"); - - string editorApiAssemblyDir = Path.Combine(apiSlnDir, ApiAssemblyNames.Editor, "bin", config); - string editorApiAssemblyFile = Path.Combine(editorApiAssemblyDir, $"{ApiAssemblyNames.Editor}.dll"); - - if (File.Exists(coreApiAssemblyFile) && File.Exists(editorApiAssemblyFile)) - return true; // The assemblies are in the output folder; assume the solution is already built - - var apiBuildInfo = new MonoBuildInfo(apiSlnFile, config); - - // TODO Replace this global NoWarn with '#pragma warning' directives on generated files, - // once we start to actively document manually maintained C# classes - apiBuildInfo.CustomProperties.Add("NoWarn=1591"); // Ignore missing documentation warnings - - if (Build(apiBuildInfo)) - return true; - - ShowBuildErrorDialog($"Failed to build {ApiAssemblyNames.SolutionName} solution."); - return false; - } - public static bool BuildProjectBlocking(string config, IEnumerable<string> godotDefines) { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) @@ -201,13 +165,13 @@ namespace GodotTools Internal.UpdateApiAssembliesFromPrebuilt(); var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); + var buildTool = (BuildTool) editorSettings.GetSetting("mono/builds/build_tool"); using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) { pr.Step("Building project solution", 0); - var buildInfo = new MonoBuildInfo(GodotSharpDirs.ProjectSlnPath, config); + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, config); // Add Godot defines string constants = buildTool == BuildTool.MsBuildVs ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; @@ -240,11 +204,28 @@ namespace GodotTools string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor"); string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player"); - CSharpProject.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath); + CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath); if (File.Exists(editorScriptsMetadataPath)) File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath); + var currentPlayRequest = GodotSharpEditor.Instance.GodotIdeManager.GodotIdeServer.CurrentPlayRequest; + + if (currentPlayRequest != null) + { + if (currentPlayRequest.Value.HasDebugger) + { + // Set the environment variable that will tell the player to connect to the IDE debugger + // TODO: We should probably add a better way to do this + Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", + "--debugger-agent=transport=dt_socket" + + $",address={currentPlayRequest.Value.DebuggerHost}:{currentPlayRequest.Value.DebuggerPort}" + + ",server=n"); + } + + return true; // Requested play from an external editor/IDE which already built the project + } + var godotDefines = new[] { Godot.OS.GetName(), diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs index 3a74fa2f66..807a20d9a1 100644 --- a/modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs @@ -7,7 +7,7 @@ using Path = System.IO.Path; namespace GodotTools { - public class MonoBuildTab : VBoxContainer + public class BuildTab : VBoxContainer { public enum BuildResults { @@ -55,7 +55,7 @@ namespace GodotTools } } - public MonoBuildInfo BuildInfo { get; private set; } + public BuildInfo BuildInfo { get; private set; } private void _LoadIssuesFromFile(string csvFile) { @@ -199,7 +199,7 @@ namespace GodotTools ErrorCount = 0; UpdateIssuesList(); - GodotSharpEditor.Instance.MonoBottomPanel.RaiseBuildTab(this); + GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); } public void OnBuildExit(BuildResults result) @@ -207,10 +207,10 @@ namespace GodotTools BuildExited = true; BuildResult = result; - _LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, GodotSharpBuilds.MsBuildIssuesFileName)); + _LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName)); UpdateIssuesList(); - GodotSharpEditor.Instance.MonoBottomPanel.RaiseBuildTab(this); + GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); } public void OnBuildExecFailed(string cause) @@ -227,7 +227,7 @@ namespace GodotTools UpdateIssuesList(); - GodotSharpEditor.Instance.MonoBottomPanel.RaiseBuildTab(this); + GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); } public void RestartBuild() @@ -235,7 +235,7 @@ namespace GodotTools if (!BuildExited) throw new InvalidOperationException("Build already started"); - GodotSharpBuilds.RestartBuild(this); + BuildManager.RestartBuild(this); } public void StopBuild() @@ -243,7 +243,7 @@ namespace GodotTools if (!BuildExited) throw new InvalidOperationException("Build is not in progress"); - GodotSharpBuilds.StopBuild(this); + BuildManager.StopBuild(this); } public override void _Ready() @@ -255,11 +255,11 @@ namespace GodotTools AddChild(issuesList); } - private MonoBuildTab() + private BuildTab() { } - public MonoBuildTab(MonoBuildInfo buildInfo) + public BuildTab(BuildInfo buildInfo) { BuildInfo = buildInfo; } diff --git a/modules/mono/editor/GodotTools/GodotTools/CSharpProject.cs b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs index 4535ed7247..c021a9051e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/CSharpProject.cs +++ b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs @@ -9,7 +9,7 @@ using Directory = GodotTools.Utils.Directory; namespace GodotTools { - public static class CSharpProject + public static class CsProjOperations { public static string GenerateGameProject(string dir, string name) { diff --git a/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs b/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs new file mode 100644 index 0000000000..4312ca0230 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs @@ -0,0 +1,11 @@ +namespace GodotTools +{ + public enum ExternalEditorId + { + None, + VisualStudio, // TODO (Windows-only) + VisualStudioForMac, // Mac-only + MonoDevelop, + VsCode + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 9b5afb94a3..099c7fcb56 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -2,16 +2,18 @@ using Godot; using GodotTools.Utils; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; +using GodotTools.Ides; using GodotTools.Internals; using GodotTools.ProjectEditor; using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; -using Path = System.IO.Path; using OS = GodotTools.Utils.OS; namespace GodotTools { + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] public class GodotSharpEditor : EditorPlugin, ISerializationListener { private EditorSettings editorSettings; @@ -24,12 +26,11 @@ namespace GodotTools private ToolButton bottomPanelBtn; - private MonoDevelopInstance monoDevelopInstance; - private MonoDevelopInstance visualStudioForMacInstance; + public GodotIdeManager GodotIdeManager { get; private set; } private WeakRef exportPluginWeak; // TODO Use WeakReference once we have proper serialization - public MonoBottomPanel MonoBottomPanel { get; private set; } + public BottomPanel BottomPanel { get; private set; } private bool CreateProjectSolution() { @@ -44,7 +45,7 @@ namespace GodotTools if (name.Empty()) name = "UnnamedProject"; - string guid = CSharpProject.GenerateGameProject(path, name); + string guid = CsProjOperations.GenerateGameProject(path, name); if (guid.Length > 0) { @@ -133,7 +134,7 @@ namespace GodotTools return; // Failed to create solution } - Instance.MonoBottomPanel.BuildProjectPressed(); + Instance.BottomPanel.BuildProjectPressed(); } public override void _Notification(int what) @@ -153,21 +154,12 @@ namespace GodotTools } } - public enum MenuOptions + private enum MenuOptions { CreateSln, AboutCSharp, } - public enum ExternalEditor - { - None, - VisualStudio, // TODO (Windows-only) - VisualStudioForMac, // Mac-only - MonoDevelop, - VsCode - } - public void ShowErrorDialog(string message, string title = "Error") { errorDialog.WindowTitle = title; @@ -184,11 +176,30 @@ namespace GodotTools public Error OpenInExternalEditor(Script script, int line, int col) { - var editor = (ExternalEditor) editorSettings.GetSetting("mono/editor/external_editor"); + var editor = (ExternalEditorId) editorSettings.GetSetting("mono/editor/external_editor"); switch (editor) { - case ExternalEditor.VsCode: + case ExternalEditorId.None: + // Tells the caller to fallback to the global external editor settings or the built-in editor + return Error.Unavailable; + case ExternalEditorId.VisualStudio: + throw new NotSupportedException(); + case ExternalEditorId.VisualStudioForMac: + goto case ExternalEditorId.MonoDevelop; + case ExternalEditorId.MonoDevelop: + { + string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath); + + if (line >= 0) + GodotIdeManager.SendOpenFile(scriptPath, line + 1, col); + else + GodotIdeManager.SendOpenFile(scriptPath); + + break; + } + + case ExternalEditorId.VsCode: { if (_vsCodePath.Empty() || !File.Exists(_vsCodePath)) { @@ -273,47 +284,6 @@ namespace GodotTools break; } - case ExternalEditor.VisualStudioForMac: - goto case ExternalEditor.MonoDevelop; - case ExternalEditor.MonoDevelop: - { - MonoDevelopInstance GetMonoDevelopInstance(string solutionPath) - { - if (OS.IsOSX() && editor == ExternalEditor.VisualStudioForMac) - { - if (visualStudioForMacInstance == null) - visualStudioForMacInstance = new MonoDevelopInstance(solutionPath, MonoDevelopInstance.EditorId.VisualStudioForMac); - - return visualStudioForMacInstance; - } - - if (monoDevelopInstance == null) - monoDevelopInstance = new MonoDevelopInstance(solutionPath, MonoDevelopInstance.EditorId.MonoDevelop); - - return monoDevelopInstance; - } - - string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath); - - if (line >= 0) - scriptPath += $";{line + 1};{col}"; - - try - { - GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath).Execute(scriptPath); - } - catch (FileNotFoundException) - { - string editorName = editor == ExternalEditor.VisualStudioForMac ? "Visual Studio" : "MonoDevelop"; - GD.PushError($"Cannot find code editor: {editorName}"); - return Error.FileNotFound; - } - - break; - } - - case ExternalEditor.None: - return Error.Unavailable; default: throw new ArgumentOutOfRangeException(); } @@ -323,12 +293,12 @@ namespace GodotTools public bool OverridesExternalEditor() { - return (ExternalEditor) editorSettings.GetSetting("mono/editor/external_editor") != ExternalEditor.None; + return (ExternalEditorId) editorSettings.GetSetting("mono/editor/external_editor") != ExternalEditorId.None; } public override bool Build() { - return GodotSharpBuilds.EditorBuildCallback(); + return BuildManager.EditorBuildCallback(); } public override void EnablePlugin() @@ -347,9 +317,9 @@ namespace GodotTools errorDialog = new AcceptDialog(); editorBaseControl.AddChild(errorDialog); - MonoBottomPanel = new MonoBottomPanel(); + BottomPanel = new BottomPanel(); - bottomPanelBtn = AddControlToBottomPanel(MonoBottomPanel, "Mono".TTR()); + bottomPanelBtn = AddControlToBottomPanel(BottomPanel, "Mono".TTR()); AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"}); @@ -407,7 +377,7 @@ namespace GodotTools if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath)) { // Make sure the existing project has Api assembly references configured correctly - CSharpProject.FixApiHintPath(GodotSharpDirs.ProjectCsProjPath); + CsProjOperations.FixApiHintPath(GodotSharpDirs.ProjectCsProjPath); } else { @@ -427,25 +397,25 @@ namespace GodotTools AddControlToContainer(CustomControlContainer.Toolbar, buildButton); // External editor settings - EditorDef("mono/editor/external_editor", ExternalEditor.None); + EditorDef("mono/editor/external_editor", ExternalEditorId.None); string settingsHintStr = "Disabled"; if (OS.IsWindows()) { - settingsHintStr += $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" + - $",Visual Studio Code:{(int) ExternalEditor.VsCode}"; + settingsHintStr += $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" + + $",Visual Studio Code:{(int) ExternalEditorId.VsCode}"; } else if (OS.IsOSX()) { - settingsHintStr += $",Visual Studio:{(int) ExternalEditor.VisualStudioForMac}" + - $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" + - $",Visual Studio Code:{(int) ExternalEditor.VsCode}"; + settingsHintStr += $",Visual Studio:{(int) ExternalEditorId.VisualStudioForMac}" + + $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" + + $",Visual Studio Code:{(int) ExternalEditorId.VsCode}"; } else if (OS.IsUnix()) { - settingsHintStr += $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" + - $",Visual Studio Code:{(int) ExternalEditor.VsCode}"; + settingsHintStr += $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" + + $",Visual Studio Code:{(int) ExternalEditorId.VsCode}"; } editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary @@ -461,7 +431,10 @@ namespace GodotTools AddExportPlugin(exportPlugin); exportPluginWeak = WeakRef(exportPlugin); - GodotSharpBuilds.Initialize(); + BuildManager.Initialize(); + + GodotIdeManager = new GodotIdeManager(); + AddChild(GodotIdeManager); } protected override void Dispose(bool disposing) @@ -478,6 +451,8 @@ namespace GodotTools exportPluginWeak.Dispose(); } + + GodotIdeManager?.Dispose(); } public void OnBeforeSerialize() diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs index b80fe1fab7..aefc51545e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs @@ -65,14 +65,14 @@ namespace GodotTools string buildConfig = isDebug ? "Debug" : "Release"; string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}"); - CSharpProject.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath); + CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath); AddFile(scriptsMetadataPath, scriptsMetadataPath); // Turn export features into defines var godotDefines = features; - if (!GodotSharpBuilds.BuildProjectBlocking(buildConfig, godotDefines)) + if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines)) { GD.PushError("Failed to build project"); return; diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 01e8c87d14..e2d576caef 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -39,26 +39,31 @@ </ItemGroup> <ItemGroup> <Compile Include="Build\MsBuildFinder.cs" /> + <Compile Include="ExternalEditorId.cs" /> + <Compile Include="Ides\GodotIdeManager.cs" /> + <Compile Include="Ides\GodotIdeServer.cs" /> + <Compile Include="Ides\MonoDevelop\EditorId.cs" /> + <Compile Include="Ides\MonoDevelop\Instance.cs" /> <Compile Include="Internals\BindingsGenerator.cs" /> <Compile Include="Internals\EditorProgress.cs" /> <Compile Include="Internals\GodotSharpDirs.cs" /> <Compile Include="Internals\Internal.cs" /> <Compile Include="Internals\ScriptClassParser.cs" /> <Compile Include="Internals\Globals.cs" /> - <Compile Include="MonoDevelopInstance.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Build\BuildSystem.cs" /> <Compile Include="Utils\Directory.cs" /> <Compile Include="Utils\File.cs" /> + <Compile Include="Utils\NotifyAwaiter.cs" /> <Compile Include="Utils\OS.cs" /> <Compile Include="GodotSharpEditor.cs" /> - <Compile Include="GodotSharpBuilds.cs" /> + <Compile Include="BuildManager.cs" /> <Compile Include="HotReloadAssemblyWatcher.cs" /> - <Compile Include="MonoBuildInfo.cs" /> - <Compile Include="MonoBuildTab.cs" /> - <Compile Include="MonoBottomPanel.cs" /> + <Compile Include="BuildInfo.cs" /> + <Compile Include="BuildTab.cs" /> + <Compile Include="BottomPanel.cs" /> <Compile Include="GodotSharpExport.cs" /> - <Compile Include="CSharpProject.cs" /> + <Compile Include="CsProjOperations.cs" /> <Compile Include="Utils\CollectionExtensions.cs" /> </ItemGroup> <ItemGroup> @@ -66,6 +71,10 @@ <Project>{6ce9a984-37b1-4f8a-8fe9-609f05f071b3}</Project> <Name>GodotTools.BuildLogger</Name> </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"> <Project>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</Project> <Name>GodotTools.ProjectEditor</Name> @@ -75,8 +84,5 @@ <Name>GodotTools.Core</Name> </ProjectReference> </ItemGroup> - <ItemGroup> - <Folder Include="Editor" /> - </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> </Project>
\ No newline at end of file diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs new file mode 100644 index 0000000000..9e24138143 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -0,0 +1,166 @@ +using System; +using System.IO; +using Godot; +using GodotTools.IdeConnection; +using GodotTools.Internals; + +namespace GodotTools.Ides +{ + public class GodotIdeManager : Node, ISerializationListener + { + public GodotIdeServer GodotIdeServer { get; private set; } + + private MonoDevelop.Instance monoDevelInstance; + private MonoDevelop.Instance vsForMacInstance; + + private GodotIdeServer GetRunningServer() + { + if (GodotIdeServer != null && !GodotIdeServer.IsDisposed) + return GodotIdeServer; + StartServer(); + return GodotIdeServer; + } + + public override void _Ready() + { + StartServer(); + } + + public void OnBeforeSerialize() + { + GodotIdeServer?.Dispose(); + } + + public void OnAfterDeserialize() + { + StartServer(); + } + + private ILogger logger; + + protected ILogger Logger + { + get => logger ?? (logger = new ConsoleLogger()); + set => logger = value; + } + + 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) + { + base.Dispose(disposing); + + GodotIdeServer?.Dispose(); + } + + private void LaunchIde() + { + var editor = (ExternalEditorId) GodotSharpEditor.Instance.GetEditorInterface() + .GetEditorSettings().GetSetting("mono/editor/external_editor"); + + switch (editor) + { + case ExternalEditorId.None: + case ExternalEditorId.VisualStudio: + case ExternalEditorId.VsCode: + throw new NotSupportedException(); + case ExternalEditorId.VisualStudioForMac: + goto case ExternalEditorId.MonoDevelop; + case ExternalEditorId.MonoDevelop: + { + MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath) + { + if (Utils.OS.IsOSX() && editor == ExternalEditorId.VisualStudioForMac) + { + vsForMacInstance = vsForMacInstance ?? + new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac); + return vsForMacInstance; + } + + monoDevelInstance = monoDevelInstance ?? + new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop); + return monoDevelInstance; + } + + try + { + var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath); + + if (!instance.IsRunning) + instance.Execute(); + } + catch (FileNotFoundException) + { + string editorName = editor == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop"; + GD.PushError($"Cannot find code editor: {editorName}"); + } + + break; + } + + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void WriteMessage(string id, params string[] arguments) + { + GetRunningServer().WriteMessage(new Message(id, arguments)); + } + + public void SendOpenFile(string file) + { + 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 + { + public void LogDebug(string message) + { + if (OS.IsStdoutVerbose()) + Console.WriteLine(message); + } + + public void LogInfo(string message) + { + if (OS.IsStdoutVerbose()) + Console.WriteLine(message); + } + + public void LogWarning(string message) + { + GD.PushWarning(message); + } + + public void LogError(string message) + { + GD.PushError(message); + } + + public void LogError(string message, Exception e) + { + GD.PushError(message + "\n" + e); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs new file mode 100644 index 0000000000..d515254e65 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs @@ -0,0 +1,208 @@ +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 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; + + // 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; + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/EditorId.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/EditorId.cs new file mode 100644 index 0000000000..1dfc91d6d1 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/EditorId.cs @@ -0,0 +1,8 @@ +namespace GodotTools.Ides.MonoDevelop +{ + public enum EditorId + { + MonoDevelop = 0, + VisualStudioForMac = 1 + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs index 61a0a992ce..1fdccf5bbd 100644 --- a/modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs @@ -1,4 +1,3 @@ -using GodotTools.Core; using System; using System.IO; using System.Collections.Generic; @@ -6,22 +5,18 @@ using System.Diagnostics; using GodotTools.Internals; using GodotTools.Utils; -namespace GodotTools +namespace GodotTools.Ides.MonoDevelop { - public class MonoDevelopInstance + public class Instance { - public enum EditorId - { - MonoDevelop = 0, - VisualStudioForMac = 1 - } - private readonly string solutionFile; private readonly EditorId editorId; private Process process; - public void Execute(params string[] files) + public bool IsRunning => process != null && !process.HasExited; + + public void Execute() { bool newWindow = process == null || process.HasExited; @@ -29,7 +24,7 @@ namespace GodotTools string command; - if (Utils.OS.IsOSX()) + if (OS.IsOSX()) { string bundleId = BundleIds[editorId]; @@ -61,16 +56,6 @@ namespace GodotTools if (newWindow) args.Add("\"" + Path.GetFullPath(solutionFile) + "\""); - foreach (var file in files) - { - int semicolonIndex = file.IndexOf(';'); - - string filePath = semicolonIndex < 0 ? file : file.Substring(0, semicolonIndex); - string cursor = semicolonIndex < 0 ? string.Empty : file.Substring(semicolonIndex); - - args.Add("\"" + Path.GetFullPath(filePath.NormalizePath()) + cursor + "\""); - } - if (command == null) throw new FileNotFoundException(); @@ -80,7 +65,7 @@ namespace GodotTools { FileName = command, Arguments = string.Join(" ", args), - UseShellExecute = false + UseShellExecute = true }); } else @@ -89,14 +74,14 @@ namespace GodotTools { FileName = command, Arguments = string.Join(" ", args), - UseShellExecute = false + UseShellExecute = true })?.Dispose(); } } - public MonoDevelopInstance(string solutionFile, EditorId editorId) + public Instance(string solutionFile, EditorId editorId) { - if (editorId == EditorId.VisualStudioForMac && !Utils.OS.IsOSX()) + if (editorId == EditorId.VisualStudioForMac && !OS.IsOSX()) throw new InvalidOperationException($"{nameof(EditorId.VisualStudioForMac)} not supported on this platform"); this.solutionFile = solutionFile; @@ -106,9 +91,9 @@ namespace GodotTools private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames; private static readonly IReadOnlyDictionary<EditorId, string> BundleIds; - static MonoDevelopInstance() + static Instance() { - if (Utils.OS.IsOSX()) + if (OS.IsOSX()) { ExecutableNames = new Dictionary<EditorId, string> { @@ -122,7 +107,7 @@ namespace GodotTools {EditorId.VisualStudioForMac, "com.microsoft.visual-studio"} }; } - else if (Utils.OS.IsWindows()) + else if (OS.IsWindows()) { ExecutableNames = new Dictionary<EditorId, string> { @@ -133,7 +118,7 @@ namespace GodotTools {EditorId.MonoDevelop, "MonoDevelop.exe"} }; } - else if (Utils.OS.IsUnix()) + else if (OS.IsUnix()) { ExecutableNames = new Dictionary<EditorId, string> { diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 9526dd3c6f..7783576910 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs @@ -46,6 +46,12 @@ namespace GodotTools.Internals public static string MonoWindowsInstallRoot => internal_MonoWindowsInstallRoot(); + public static void EditorRunPlay() => internal_EditorRunPlay(); + + public static void EditorRunStop() => internal_EditorRunStop(); + + public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts(); + // Internal Calls [MethodImpl(MethodImplOptions.InternalCall)] @@ -95,5 +101,14 @@ namespace GodotTools.Internals [MethodImpl(MethodImplOptions.InternalCall)] private static extern string internal_MonoWindowsInstallRoot(); + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void internal_EditorRunPlay(); + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void internal_EditorRunStop(); + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void internal_ScriptEditorDebugger_ReloadScripts(); } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs index 288c65de74..e3c2c822a5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; namespace GodotTools.Utils { @@ -17,5 +18,12 @@ namespace GodotTools.Utils return orElse; } + + public static IEnumerable<string> EnumerateLines(this TextReader textReader) + { + string line; + while ((line = textReader.ReadLine()) != null) + yield return line; + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs new file mode 100644 index 0000000000..a3490fa89f --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.CompilerServices; + +namespace GodotTools.Utils +{ + public sealed class NotifyAwaiter<T> : INotifyCompletion + { + private Action continuation; + private Exception exception; + private T result; + + public bool IsCompleted { get; private set; } + + public T GetResult() + { + if (exception != null) + throw exception; + return result; + } + + public void OnCompleted(Action continuation) + { + if (this.continuation != null) + throw new InvalidOperationException("This awaiter has already been listened"); + this.continuation = continuation; + } + + public void SetResult(T result) + { + if (IsCompleted) + throw new InvalidOperationException("This awaiter is already completed"); + + IsCompleted = true; + this.result = result; + + continuation?.Invoke(); + } + + public void SetException(Exception exception) + { + if (IsCompleted) + throw new InvalidOperationException("This awaiter is already completed"); + + IsCompleted = true; + this.exception = exception; + + continuation?.Invoke(); + } + + public NotifyAwaiter<T> Reset() + { + continuation = null; + exception = null; + result = default; + IsCompleted = false; + return this; + } + + public NotifyAwaiter<T> GetAwaiter() + { + return this; + } + } +} diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp index 0014aaca70..7db1090e2a 100644 --- a/modules/mono/editor/editor_internal_calls.cpp +++ b/modules/mono/editor/editor_internal_calls.cpp @@ -350,6 +350,21 @@ MonoString *godot_icall_Internal_MonoWindowsInstallRoot() { #endif } +void godot_icall_Internal_EditorRunPlay() { + EditorNode::get_singleton()->run_play(); +} + +void godot_icall_Internal_EditorRunStop() { + EditorNode::get_singleton()->run_stop(); +} + +void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts() { + ScriptEditorDebugger *sed = ScriptEditor::get_singleton()->get_debugger(); + if (sed) { + sed->reload_scripts(); + } +} + MonoString *godot_icall_Utils_OS_GetPlatformName() { String os_name = OS::get_singleton()->get_name(); return GDMonoMarshal::mono_string_from_godot(os_name); @@ -414,7 +429,9 @@ void register_editor_internal_calls() { mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorEdit", (void *)godot_icall_Internal_ScriptEditorEdit); mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorNodeShowScriptScreen", (void *)godot_icall_Internal_EditorNodeShowScriptScreen); mono_add_internal_call("GodotTools.Internals.Internal::internal_GetScriptsMetadataOrNothing", (void *)godot_icall_Internal_GetScriptsMetadataOrNothing); - mono_add_internal_call("GodotTools.Internals.Internal::internal_MonoWindowsInstallRoot", (void *)godot_icall_Internal_MonoWindowsInstallRoot); + 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_ScriptEditorDebugger_ReloadScripts", (void *)godot_icall_Internal_ScriptEditorDebugger_ReloadScripts); // Globals mono_add_internal_call("GodotTools.Internals.Globals::internal_EditorScale", (void *)godot_icall_Globals_EditorScale); diff --git a/modules/mono/glue/Managed/Files/Dispatcher.cs b/modules/mono/glue/Managed/Files/Dispatcher.cs new file mode 100644 index 0000000000..072e0f20ff --- /dev/null +++ b/modules/mono/glue/Managed/Files/Dispatcher.cs @@ -0,0 +1,13 @@ +using System.Runtime.CompilerServices; + +namespace Godot +{ + public static class Dispatcher + { + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern GodotTaskScheduler godot_icall_DefaultGodotTaskScheduler(); + + public static GodotSynchronizationContext SynchronizationContext => + godot_icall_DefaultGodotTaskScheduler().Context; + } +} diff --git a/modules/mono/glue/Managed/Files/GodotSynchronizationContext.cs b/modules/mono/glue/Managed/Files/GodotSynchronizationContext.cs index e727781d63..4b5e3f8761 100644 --- a/modules/mono/glue/Managed/Files/GodotSynchronizationContext.cs +++ b/modules/mono/glue/Managed/Files/GodotSynchronizationContext.cs @@ -6,17 +6,16 @@ namespace Godot { public class GodotSynchronizationContext : SynchronizationContext { - private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> queue = new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>(); + private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> _queue = new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>(); public override void Post(SendOrPostCallback d, object state) { - queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state)); + _queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state)); } public void ExecutePendingContinuations() { - KeyValuePair<SendOrPostCallback, object> workItem; - while (queue.TryTake(out workItem)) + while (_queue.TryTake(out var workItem)) { workItem.Key(workItem.Value); } diff --git a/modules/mono/glue/Managed/Files/GodotTaskScheduler.cs b/modules/mono/glue/Managed/Files/GodotTaskScheduler.cs index 9a40fef5a9..8eaeea50dc 100644 --- a/modules/mono/glue/Managed/Files/GodotTaskScheduler.cs +++ b/modules/mono/glue/Managed/Files/GodotTaskScheduler.cs @@ -8,7 +8,7 @@ namespace Godot { public class GodotTaskScheduler : TaskScheduler { - private GodotSynchronizationContext Context { get; set; } + internal GodotSynchronizationContext Context { get; } private readonly LinkedList<Task> _tasks = new LinkedList<Task>(); public GodotTaskScheduler() @@ -28,14 +28,10 @@ namespace Godot protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { if (SynchronizationContext.Current != Context) - { return false; - } if (taskWasPreviouslyQueued) - { TryDequeue(task); - } return TryExecuteTask(task); } @@ -52,7 +48,8 @@ namespace Godot { lock (_tasks) { - return _tasks.ToArray(); + foreach (Task task in _tasks) + yield return task; } } diff --git a/modules/mono/glue/Managed/Managed.csproj b/modules/mono/glue/Managed/Managed.csproj index 61f738922b..ad55fe9539 100644 --- a/modules/mono/glue/Managed/Managed.csproj +++ b/modules/mono/glue/Managed/Managed.csproj @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -37,4 +37,4 @@ <Compile Include="Properties\AssemblyInfo.cs" /> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> -</Project> +</Project>
\ No newline at end of file diff --git a/modules/mono/glue/gd_glue.cpp b/modules/mono/glue/gd_glue.cpp index 7c30092855..c44474ea53 100644 --- a/modules/mono/glue/gd_glue.cpp +++ b/modules/mono/glue/gd_glue.cpp @@ -211,6 +211,10 @@ MonoString *godot_icall_GD_var2str(MonoObject *p_var) { return GDMonoMarshal::mono_string_from_godot(vars); } +MonoObject *godot_icall_DefaultGodotTaskScheduler() { + return GDMonoUtils::mono_cache.task_scheduler_handle->get_target(); +} + void godot_register_gd_icalls() { mono_add_internal_call("Godot.GD::godot_icall_GD_bytes2var", (void *)godot_icall_GD_bytes2var); mono_add_internal_call("Godot.GD::godot_icall_GD_convert", (void *)godot_icall_GD_convert); @@ -234,6 +238,9 @@ void godot_register_gd_icalls() { mono_add_internal_call("Godot.GD::godot_icall_GD_type_exists", (void *)godot_icall_GD_type_exists); mono_add_internal_call("Godot.GD::godot_icall_GD_var2bytes", (void *)godot_icall_GD_var2bytes); mono_add_internal_call("Godot.GD::godot_icall_GD_var2str", (void *)godot_icall_GD_var2str); + + // Dispatcher + mono_add_internal_call("Godot.Dispatcher::godot_icall_DefaultGodotTaskScheduler", (void *)godot_icall_DefaultGodotTaskScheduler); } #endif // MONO_GLUE_ENABLED diff --git a/modules/mono/glue/gd_glue.h b/modules/mono/glue/gd_glue.h index d4e20e2887..a34c0bc50f 100644 --- a/modules/mono/glue/gd_glue.h +++ b/modules/mono/glue/gd_glue.h @@ -75,6 +75,8 @@ MonoArray *godot_icall_GD_var2bytes(MonoObject *p_var, MonoBoolean p_full_object MonoString *godot_icall_GD_var2str(MonoObject *p_var); +MonoObject *godot_icall_DefaultGodotTaskScheduler(); + // Register internal calls void godot_register_gd_icalls(); diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index 45f79074be..19704d3521 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -561,14 +561,14 @@ bool GDMono::_load_corlib_assembly() { } #ifdef TOOLS_ENABLED -bool GDMono::copy_prebuilt_api_assembly(APIAssembly::Type p_api_type) { +bool GDMono::copy_prebuilt_api_assembly(APIAssembly::Type p_api_type, const String &p_config) { bool &api_assembly_out_of_sync = (p_api_type == APIAssembly::API_CORE) ? GDMono::get_singleton()->core_api_assembly_out_of_sync : GDMono::get_singleton()->editor_api_assembly_out_of_sync; - String src_dir = GodotSharpDirs::get_data_editor_prebuilt_api_dir().plus_file("Debug"); - String dst_dir = GodotSharpDirs::get_res_assemblies_dir(); + String src_dir = GodotSharpDirs::get_data_editor_prebuilt_api_dir().plus_file(p_config); + String dst_dir = GodotSharpDirs::get_res_assemblies_base_dir().plus_file(p_config); String assembly_name = p_api_type == APIAssembly::API_CORE ? CORE_API_ASSEMBLY_NAME : EDITOR_API_ASSEMBLY_NAME; @@ -631,18 +631,28 @@ String GDMono::update_api_assemblies_from_prebuilt() { if (!api_assembly_out_of_sync && FileAccess::exists(core_assembly_path) && FileAccess::exists(editor_assembly_path)) return String(); // No update needed - print_verbose("Updating API assemblies"); + const int CONFIGS_LEN = 2; + String configs[CONFIGS_LEN] = { String("Debug"), String("Release") }; - String prebuilt_api_dir = GodotSharpDirs::get_data_editor_prebuilt_api_dir().plus_file("Debug"); - String prebuilt_core_dll_path = prebuilt_api_dir.plus_file(CORE_API_ASSEMBLY_NAME ".dll"); - String prebuilt_editor_dll_path = prebuilt_api_dir.plus_file(EDITOR_API_ASSEMBLY_NAME ".dll"); + for (int i = 0; i < CONFIGS_LEN; i++) { + String config = configs[i]; - if (!FileAccess::exists(prebuilt_core_dll_path) || !FileAccess::exists(prebuilt_editor_dll_path)) - return FAIL_REASON(api_assembly_out_of_sync, /* prebuilt_exists: */ false); + print_verbose("Updating '" + config + "' API assemblies"); - // Copy the prebuilt Api - if (!copy_prebuilt_api_assembly(APIAssembly::API_CORE) || !copy_prebuilt_api_assembly(APIAssembly::API_EDITOR)) - return FAIL_REASON(api_assembly_out_of_sync, /* prebuilt_exists: */ true); + String prebuilt_api_dir = GodotSharpDirs::get_data_editor_prebuilt_api_dir().plus_file(config); + String prebuilt_core_dll_path = prebuilt_api_dir.plus_file(CORE_API_ASSEMBLY_NAME ".dll"); + String prebuilt_editor_dll_path = prebuilt_api_dir.plus_file(EDITOR_API_ASSEMBLY_NAME ".dll"); + + if (!FileAccess::exists(prebuilt_core_dll_path) || !FileAccess::exists(prebuilt_editor_dll_path)) { + return FAIL_REASON(api_assembly_out_of_sync, /* prebuilt_exists: */ false); + } + + // Copy the prebuilt Api + if (!copy_prebuilt_api_assembly(APIAssembly::API_CORE, config) || + !copy_prebuilt_api_assembly(APIAssembly::API_EDITOR, config)) { + return FAIL_REASON(api_assembly_out_of_sync, /* prebuilt_exists: */ true); + } + } return String(); // Updated successfully diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h index c5bcce4fa1..4f7d3791f7 100644 --- a/modules/mono/mono_gd/gd_mono.h +++ b/modules/mono/mono_gd/gd_mono.h @@ -165,7 +165,7 @@ public: #endif #ifdef TOOLS_ENABLED - bool copy_prebuilt_api_assembly(APIAssembly::Type p_api_type); + bool copy_prebuilt_api_assembly(APIAssembly::Type p_api_type, const String &p_config); String update_api_assemblies_from_prebuilt(); #endif |