Unverified Commit 54b20a25 by Ignacio Roldán Etcheverry Committed by GitHub

Merge pull request #38600 from neikeq/no

Switch to nuget Microsoft.Build and rewrite GodotTools messasing protocol
parents da898c11 3ce09246
......@@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Security;
using Microsoft.Build.Framework;
using GodotTools.Core;
namespace GodotTools.BuildLogger
{
......@@ -183,4 +182,17 @@ namespace GodotTools.BuildLogger
private StreamWriter issuesStreamWriter;
private int indent;
}
internal static class StringExtensions
{
public static string CsvEscape(this string value, char delimiter = ',')
{
bool hasSpecialChar = value.IndexOfAny(new[] { '\"', '\n', '\r', delimiter }) != -1;
if (hasSpecialChar)
return "\"" + value.Replace("\"", "\"\"") + "\"";
return value;
}
}
}
<?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')" />
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GodotTools.BuildLogger</RootNamespace>
<AssemblyName>GodotTools.BuildLogger</AssemblyName>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<LangVersion>7</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.2</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="Microsoft.Build.Framework" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="GodotBuildLogger.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj">
<Project>{639e48bd-44e5-4091-8edd-22d36dc0768d}</Project>
<Name>GodotTools.Core</Name>
</ProjectReference>
<PackageReference Include="Microsoft.Build.Framework" Version="16.5.0" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
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.BuildLogger")]
[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("6CE9A984-37B1-4F8A-8FE9-609F05F071B3")]
// 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")]
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>GodotTools.Core</RootNamespace>
<AssemblyName>GodotTools.Core</AssemblyName>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
<LangVersion>7</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="FileUtils.cs" />
<Compile Include="ProcessExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="StringExtensions.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
using System.Reflection;
using System.Runtime.CompilerServices;
// Information about this assembly is defined by the following attributes.
// Change them to the values specific to your project.
[assembly: AssemblyTitle("GodotTools.Core")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("")]
[assembly: AssemblyCopyright("Godot Engine contributors")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
// The form "{Major}.{Minor}.*" will automatically update the build and revision,
// and "{Major}.{Minor}.{Build}.*" will update just the revision.
[assembly: AssemblyVersion("1.0.*")]
// The following attributes are used to specify the signing key for the assembly,
// if desired. See the Mono documentation for more information about signing.
//[assembly: AssemblyDelaySign(false)]
//[assembly: AssemblyKeyFile("")]
......@@ -33,23 +33,13 @@ namespace GodotTools.Core
return rooted ? Path.DirectorySeparatorChar + path : path;
}
private static readonly string driveRoot = Path.GetPathRoot(Environment.CurrentDirectory);
private static readonly string DriveRoot = Path.GetPathRoot(Environment.CurrentDirectory);
public static bool IsAbsolutePath(this string path)
{
return path.StartsWith("/", StringComparison.Ordinal) ||
path.StartsWith("\\", StringComparison.Ordinal) ||
path.StartsWith(driveRoot, StringComparison.Ordinal);
}
public static string CsvEscape(this string value, char delimiter = ',')
{
bool hasSpecialChar = value.IndexOfAny(new char[] { '\"', '\n', '\r', delimiter }) != -1;
if (hasSpecialChar)
return "\"" + value.Replace("\"", "\"\"") + "\"";
return value;
path.StartsWith(DriveRoot, StringComparison.Ordinal);
}
public static string ToSafeDirName(this string dirName, bool allowDirSeparator)
......
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);
}
}
}
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();
}
}
}
}
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);
}
}
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();
}
}
}
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;
}
}
}
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;
}
}
}
<?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>
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)}')";
}
}
}
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();
}
}
}
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;
}
}
}
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")]
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} =======");
}
}
}
<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>
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);
}
}
}
}
}
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;
}
}
}
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);
}
}
namespace GodotTools.IdeConnection
namespace GodotTools.IdeMessaging
{
public struct GodotIdeMetadata
public readonly struct GodotIdeMetadata
{
public int Port { get; }
public string EditorExecutablePath { get; }
public const string DefaultFileName = "ide_messaging_meta.txt";
public GodotIdeMetadata(int port, string editorExecutablePath)
{
Port = port;
......
<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>
namespace GodotTools.IdeMessaging
{
public interface IHandshake
{
string GetHandshakeLine(string identity);
bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger);
}
}
using System;
namespace GodotTools.IdeConnection
namespace GodotTools.IdeMessaging
{
public interface ILogger
{
......
using System.Threading.Tasks;
namespace GodotTools.IdeMessaging
{
public interface IMessageHandler
{
Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger);
}
}
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;
}
}
}
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;
}
}
}
// 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
{
}
}
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});
}
}
}
using System;
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 Exception exception;
......
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();
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>GodotTools.ProjectEditor</RootNamespace>
<AssemblyName>GodotTools.ProjectEditor</AssemblyName>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
<BaseIntermediateOutputPath>obj</BaseIntermediateOutputPath>
<LangVersion>7</LangVersion>
<TargetFramework>net472</TargetFramework>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="Microsoft.Build" />
<Reference Include="DotNet.Glob, Version=2.1.1.0, Culture=neutral, PublicKeyToken=b68cc888b4f632d1, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\DotNet.Glob.2.1.1\lib\net45\DotNet.Glob.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="ApiAssembliesInfo.cs" />
<Compile Include="DotNetSolution.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="IdentifierUtils.cs" />
<Compile Include="ProjectExtensions.cs" />
<Compile Include="ProjectGenerator.cs" />
<Compile Include="ProjectUtils.cs" />
<PackageReference Include="Microsoft.Build" Version="16.5.0" />
<PackageReference Include="Microsoft.Build.Runtime" Version="16.5.0" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj">
<Project>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</Project>
<Name>GodotTools.Core</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<!--
The 'Microsoft.Build.Runtime' package includes an mscorlib reference assembly in contentFiles.
This causes our project build to fail. As a workaround, we remove {CandidateAssemblyFiles}
from AssemblySearchPaths as described here: https://github.com/microsoft/msbuild/issues/3486.
-->
<AssemblySearchPaths>$([System.String]::Copy('$(AssemblySearchPaths)').Replace('{CandidateAssemblyFiles}', ''))</AssemblySearchPaths>
<AssemblySearchPaths Condition=" '$(MSBuildRuntimeVersion)' != '' ">$(AssemblySearchPaths.Split(';'))</AssemblySearchPaths>
</PropertyGroup>
</Project>
......@@ -2,8 +2,8 @@ using GodotTools.Core;
using System;
using System.Collections.Generic;
using System.IO;
using DotNet.Globbing;
using Microsoft.Build.Construction;
using Microsoft.Build.Globbing;
namespace GodotTools.ProjectEditor
{
......@@ -11,8 +11,6 @@ namespace GodotTools.ProjectEditor
{
public static ProjectItemElement FindItemOrNull(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
{
GlobOptions globOptions = new GlobOptions {Evaluation = {CaseInsensitive = false}};
string normalizedInclude = include.NormalizePath();
foreach (var itemGroup in root.ItemGroups)
......@@ -25,7 +23,8 @@ namespace GodotTools.ProjectEditor
if (item.ItemType != itemType)
continue;
var glob = Glob.Parse(item.Include.NormalizePath(), globOptions);
//var glob = Glob.Parse(item.Include.NormalizePath(), globOptions);
var glob = MSBuildGlob.Parse(item.Include.NormalizePath());
if (glob.IsMatch(normalizedInclude))
return item;
......@@ -36,8 +35,6 @@ namespace GodotTools.ProjectEditor
}
public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
{
GlobOptions globOptions = new GlobOptions {Evaluation = {CaseInsensitive = false}};
string normalizedInclude = Path.GetFullPath(include).NormalizePath();
foreach (var itemGroup in root.ItemGroups)
......@@ -50,7 +47,7 @@ namespace GodotTools.ProjectEditor
if (item.ItemType != itemType)
continue;
var glob = Glob.Parse(Path.GetFullPath(item.Include).NormalizePath(), globOptions);
var glob = MSBuildGlob.Parse(Path.GetFullPath(item.Include).NormalizePath());
if (glob.IsMatch(normalizedInclude))
return item;
......
......@@ -4,8 +4,8 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using DotNet.Globbing;
using Microsoft.Build.Construction;
using Microsoft.Build.Globbing;
namespace GodotTools.ProjectEditor
{
......@@ -133,9 +133,6 @@ namespace GodotTools.ProjectEditor
var result = new List<string>();
var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs");
var globOptions = new GlobOptions();
globOptions.Evaluation.CaseInsensitive = false;
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
......@@ -151,7 +148,7 @@ namespace GodotTools.ProjectEditor
string normalizedInclude = item.Include.NormalizePath();
var glob = Glob.Parse(normalizedInclude, globOptions);
var glob = MSBuildGlob.Parse(normalizedInclude);
// TODO Check somehow if path has no blob to avoid the following loop...
......
using System.Reflection;
using System.Runtime.CompilerServices;
// Information about this assembly is defined by the following attributes.
// Change them to the values specific to your project.
[assembly: AssemblyTitle("GodotTools.ProjectEditor")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("")]
[assembly: AssemblyCopyright("Godot Engine contributors")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
// The form "{Major}.{Minor}.*" will automatically update the build and revision,
// and "{Major}.{Minor}.{Build}.*" will update just the revision.
[assembly: AssemblyVersion("1.0.*")]
// The following attributes are used to specify the signing key for the assembly,
// if desired. See the Mono documentation for more information about signing.
//[assembly: AssemblyDelaySign(false)]
//[assembly: AssemblyKeyFile("")]
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="DotNet.Glob" version="2.1.1" targetFramework="net45" />
</packages>
......@@ -9,7 +9,7 @@ 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}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.IdeMessaging", "GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj", "{92600954-25F0-4291-8E11-1FEE9FC4BE20}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
......
......@@ -109,9 +109,9 @@ namespace GodotTools.Build
buildInfo.LogsDirPath, buildInfo.CustomProperties);
}
public static async Task<int> BuildAsync(BuildInfo buildInfo)
public static Task<int> BuildAsync(BuildInfo buildInfo)
{
return await BuildAsync(buildInfo.Solution, buildInfo.Configuration,
return BuildAsync(buildInfo.Solution, buildInfo.Configuration,
buildInfo.LogsDirPath, buildInfo.CustomProperties);
}
......
......@@ -28,15 +28,13 @@ namespace GodotTools.Build
{
case BuildManager.BuildTool.MsBuildVs:
{
if (_msbuildToolsPath.Empty() || !File.Exists(_msbuildToolsPath))
if (string.IsNullOrEmpty(_msbuildToolsPath) || !File.Exists(_msbuildToolsPath))
{
// Try to search it again if it wasn't found last time or if it was removed from its location
_msbuildToolsPath = FindMsBuildToolsPathOnWindows();
if (_msbuildToolsPath.Empty())
{
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildVs}'.");
}
if (string.IsNullOrEmpty(_msbuildToolsPath))
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'.");
}
if (!_msbuildToolsPath.EndsWith("\\"))
......@@ -49,44 +47,48 @@ namespace GodotTools.Build
string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat");
if (!File.Exists(msbuildPath))
{
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildMono}'. Tried with path: {msbuildPath}");
}
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}");
return msbuildPath;
}
case BuildManager.BuildTool.JetBrainsMsBuild:
{
var editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName);
if (!File.Exists(editorPath))
throw new FileNotFoundException($"Cannot find Rider executable. Tried with path: {editorPath}");
var riderDir = new FileInfo(editorPath).Directory.Parent;
return Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe");
var riderDir = new FileInfo(editorPath).Directory?.Parent;
string msbuildPath = Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe");
if (!File.Exists(msbuildPath))
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildJetBrains}'. Tried with path: {msbuildPath}");
return msbuildPath;
}
default:
throw new IndexOutOfRangeException("Invalid build tool in editor settings");
}
}
if (OS.IsUnixLike())
if (OS.IsUnixLike)
{
if (buildTool == BuildManager.BuildTool.MsBuildMono)
{
if (_msbuildUnixPath.Empty() || !File.Exists(_msbuildUnixPath))
if (string.IsNullOrEmpty(_msbuildUnixPath) || !File.Exists(_msbuildUnixPath))
{
// Try to search it again if it wasn't found last time or if it was removed from its location
_msbuildUnixPath = FindBuildEngineOnUnix("msbuild");
}
if (_msbuildUnixPath.Empty())
{
throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMsbuildMono}'");
}
if (string.IsNullOrEmpty(_msbuildUnixPath))
throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'");
return _msbuildUnixPath;
}
else
{
throw new IndexOutOfRangeException("Invalid build tool in editor settings");
}
throw new IndexOutOfRangeException("Invalid build tool in editor settings");
}
throw new PlatformNotSupportedException();
......@@ -114,12 +116,12 @@ namespace GodotTools.Build
{
string ret = OS.PathWhich(name);
if (!ret.Empty())
if (!string.IsNullOrEmpty(ret))
return ret;
string retFallback = OS.PathWhich($"{name}.exe");
if (!retFallback.Empty())
if (!string.IsNullOrEmpty(retFallback))
return retFallback;
foreach (string hintDir in MsBuildHintDirs)
......@@ -143,7 +145,7 @@ namespace GodotTools.Build
string vsWherePath = Environment.GetEnvironmentVariable(Internal.GodotIs32Bits() ? "ProgramFiles" : "ProgramFiles(x86)");
vsWherePath += "\\Microsoft Visual Studio\\Installer\\vswhere.exe";
var vsWhereArgs = new[] { "-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild" };
var vsWhereArgs = new[] {"-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"};
var outputArray = new Godot.Collections.Array<string>();
int exitCode = Godot.OS.Execute(vsWherePath, vsWhereArgs,
......@@ -171,7 +173,7 @@ namespace GodotTools.Build
string value = line.Substring(sepIdx + 1).StripEdges();
if (value.Empty())
if (string.IsNullOrEmpty(value))
throw new FormatException("installationPath value is empty");
if (!value.EndsWith("\\"))
......
......@@ -15,9 +15,9 @@ namespace GodotTools
{
private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>();
public const string PropNameMsbuildMono = "MSBuild (Mono)";
public const string PropNameMsbuildVs = "MSBuild (VS Build Tools)";
public const string PropNameMsbuildJetBrains = "MSBuild (JetBrains Rider)";
public const string PropNameMSBuildMono = "MSBuild (Mono)";
public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)";
public const string PropNameMSBuildJetBrains = "MSBuild (JetBrains Rider)";
public const string MsBuildIssuesFileName = "msbuild_issues.csv";
public const string MsBuildLogFileName = "msbuild_log.txt";
......@@ -219,7 +219,7 @@ namespace GodotTools
if (File.Exists(editorScriptsMetadataPath))
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
var currentPlayRequest = GodotSharpEditor.Instance.GodotIdeManager.GodotIdeServer.CurrentPlayRequest;
var currentPlayRequest = GodotSharpEditor.Instance.CurrentPlaySettings;
if (currentPlayRequest != null)
{
......@@ -233,7 +233,8 @@ namespace GodotTools
",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[]
......@@ -251,9 +252,7 @@ namespace GodotTools
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
var msbuild = BuildTool.MsBuildMono;
if (OS.IsWindows)
msbuild = RiderPathManager.IsExternalEditorSetToRider(editorSettings)
? BuildTool.JetBrainsMsBuild
: BuildTool.MsBuildVs;
msbuild = RiderPathManager.IsExternalEditorSetToRider(editorSettings) ? BuildTool.JetBrainsMsBuild : BuildTool.MsBuildVs;
EditorDef("mono/builds/build_tool", msbuild);
......@@ -263,8 +262,8 @@ namespace GodotTools
["name"] = "mono/builds/build_tool",
["hint"] = Godot.PropertyHint.Enum,
["hint_string"] = OS.IsWindows ?
$"{PropNameMsbuildMono},{PropNameMsbuildVs},{PropNameMsbuildJetBrains}" :
$"{PropNameMsbuildMono}"
$"{PropNameMSBuildMono},{PropNameMSBuildVs},{PropNameMSBuildJetBrains}" :
$"{PropNameMSBuildMono}"
});
EditorDef("mono/builds/print_build_output", false);
......
......@@ -72,7 +72,7 @@ namespace GodotTools
{
string[] csvColumns = file.GetCsvLine();
if (csvColumns.Length == 1 && csvColumns[0].Empty())
if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0]))
return;
if (csvColumns.Length != 7)
......@@ -115,12 +115,12 @@ namespace GodotTools
// Get correct issue idx from issue list
int issueIndex = (int)issuesList.GetItemMetadata(idx);
if (idx < 0 || idx >= issues.Count)
if (issueIndex < 0 || issueIndex >= issues.Count)
throw new IndexOutOfRangeException("Issue index out of range");
BuildIssue issue = issues[issueIndex];
if (issue.ProjectFile.Empty() && issue.File.Empty())
if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File))
return;
string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : BuildInfo.Solution.GetBaseDir();
......@@ -158,14 +158,14 @@ namespace GodotTools
string tooltip = string.Empty;
tooltip += $"Message: {issue.Message}";
if (!issue.Code.Empty())
if (!string.IsNullOrEmpty(issue.Code))
tooltip += $"\nCode: {issue.Code}";
tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}";
string text = string.Empty;
if (!issue.File.Empty())
if (!string.IsNullOrEmpty(issue.File))
{
text += $"{issue.File}({issue.Line},{issue.Column}): ";
......@@ -174,7 +174,7 @@ namespace GodotTools
tooltip += $"\nColumn: {issue.Column}";
}
if (!issue.ProjectFile.Empty())
if (!string.IsNullOrEmpty(issue.ProjectFile))
tooltip += $"\nProject: {issue.ProjectFile}";
text += issue.Message;
......
......@@ -587,7 +587,7 @@ MONO_AOT_MODE_LAST = 1000,
string arch = "x86_64";
return $"{platform}-{arch}";
}
case OS.Platforms.X11:
case OS.Platforms.LinuxBSD:
case OS.Platforms.Server:
{
string arch = bits == "64" ? "x86_64" : "i686";
......
......@@ -414,7 +414,7 @@ namespace GodotTools.Export
case OS.Platforms.UWP:
return "net_4_x_win";
case OS.Platforms.OSX:
case OS.Platforms.X11:
case OS.Platforms.LinuxBSD:
case OS.Platforms.Server:
case OS.Platforms.Haiku:
return "net_4_x";
......
......@@ -37,6 +37,8 @@ namespace GodotTools
public BottomPanel BottomPanel { get; private set; }
public PlaySettings? CurrentPlaySettings { get; set; }
public static string ProjectAssemblyName
{
get
......@@ -228,12 +230,12 @@ namespace GodotTools
[UsedImplicitly]
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:
// 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;
case ExternalEditorId.VisualStudio:
throw new NotSupportedException();
......@@ -249,17 +251,20 @@ namespace GodotTools
{
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
if (line >= 0)
GodotIdeManager.SendOpenFile(scriptPath, line + 1, col);
else
GodotIdeManager.SendOpenFile(scriptPath);
GodotIdeManager.LaunchIdeAsync().ContinueWith(launchTask =>
{
var editorPick = launchTask.Result;
if (line >= 0)
editorPick?.SendOpenFile(scriptPath, line + 1, col);
else
editorPick?.SendOpenFile(scriptPath);
});
break;
}
case ExternalEditorId.VsCode:
{
if (_vsCodePath.Empty() || !File.Exists(_vsCodePath))
if (string.IsNullOrEmpty(_vsCodePath) || !File.Exists(_vsCodePath))
{
// Try to search it again if it wasn't found last time or if it was removed from its location
_vsCodePath = VsCodeNames.SelectFirstNotNull(OS.PathWhich, orElse: string.Empty);
......@@ -300,7 +305,7 @@ namespace GodotTools
if (line >= 0)
{
args.Add("-g");
args.Add($"{scriptPath}:{line + 1}:{col}");
args.Add($"{scriptPath}:{line}:{col}");
}
else
{
......@@ -311,7 +316,7 @@ namespace GodotTools
if (OS.IsOSX)
{
if (!osxAppBundleInstalled && _vsCodePath.Empty())
if (!osxAppBundleInstalled && string.IsNullOrEmpty(_vsCodePath))
{
GD.PushError("Cannot find code editor: VSCode");
return Error.FileNotFound;
......@@ -321,7 +326,7 @@ namespace GodotTools
}
else
{
if (_vsCodePath.Empty())
if (string.IsNullOrEmpty(_vsCodePath))
{
GD.PushError("Cannot find code editor: VSCode");
return Error.FileNotFound;
......@@ -341,7 +346,6 @@ namespace GodotTools
break;
}
default:
throw new ArgumentOutOfRangeException();
}
......@@ -505,7 +509,7 @@ namespace GodotTools
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
$",JetBrains Rider:{(int)ExternalEditorId.Rider}";
}
else if (OS.IsUnixLike())
else if (OS.IsUnixLike)
{
settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
......
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{27B00618-A6F2-4828-B922-05CAEB08C286}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>GodotTools</RootNamespace>
<AssemblyName>GodotTools</AssemblyName>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
<LangVersion>7</LangVersion>
<TargetFramework>net472</TargetFramework>
<LangVersion>7.2</LangVersion>
<GodotApiConfiguration>Debug</GodotApiConfiguration> <!-- The Godot editor uses the Debug Godot API assemblies -->
<GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
<GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir>
......@@ -18,32 +12,12 @@
<!-- The project is part of the Godot source tree -->
<!-- Use the Godot source tree output folder instead of '$(ProjectDir)/bin' -->
<OutputPath>$(GodotOutputDataDir)/Tools</OutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<DefineConstants>DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
<!-- Must not append '$(TargetFramework)' to the output path in this case -->
<AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
<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>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<Reference Include="GodotSharp">
<HintPath>$(GodotApiAssembliesDir)/GodotSharp.dll</HintPath>
<Private>False</Private>
......@@ -54,58 +28,9 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Build\MsBuildFinder.cs" />
<Compile Include="Export\AotBuilder.cs" />
<Compile Include="Export\ExportPlugin.cs" />
<Compile Include="Export\XcodeHelper.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="Ides\Rider\RiderPathLocator.cs" />
<Compile Include="Ides\Rider\RiderPathManager.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="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="BuildManager.cs" />
<Compile Include="HotReloadAssemblyWatcher.cs" />
<Compile Include="BuildInfo.cs" />
<Compile Include="BuildTab.cs" />
<Compile Include="BottomPanel.cs" />
<Compile Include="CsProjOperations.cs" />
<Compile Include="Utils\CollectionExtensions.cs" />
<Compile Include="Utils\User32Dll.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj">
<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>
</ProjectReference>
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj">
<Project>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</Project>
<Name>GodotTools.Core</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" />
<ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" />
<ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" />
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
using System;
using System.IO;
using System.Threading.Tasks;
using Godot;
using GodotTools.IdeConnection;
using GodotTools.IdeMessaging;
using GodotTools.IdeMessaging.Requests;
using GodotTools.Internals;
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 vsForMacInstance;
private GodotIdeServer GetRunningServer()
private MessagingServer GetRunningOrNewServer()
{
if (GodotIdeServer != null && !GodotIdeServer.IsDisposed)
return GodotIdeServer;
StartServer();
return GodotIdeServer;
if (MessagingServer != null && !MessagingServer.IsDisposed)
return MessagingServer;
MessagingServer?.Dispose();
MessagingServer = new MessagingServer(OS.GetExecutablePath(), ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir), new GodotLogger());
_ = MessagingServer.Listen();
return MessagingServer;
}
public override void _Ready()
{
StartServer();
_ = GetRunningOrNewServer();
}
public void OnBeforeSerialize()
{
GodotIdeServer?.Dispose();
}
public void OnAfterDeserialize()
{
StartServer();
_ = GetRunningOrNewServer();
}
private ILogger logger;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
protected ILogger Logger
if (disposing)
{
MessagingServer?.Dispose();
}
}
private string GetExternalEditorIdentity(ExternalEditorId editorId)
{
get => logger ?? (logger = new GodotLogger());
// Manually convert to string to avoid breaking compatibility in case we rename the enum fields.
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();
}
}
private void StartServer()
public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000)
{
GodotIdeServer?.Dispose();
GodotIdeServer = new GodotIdeServer(LaunchIde,
OS.GetExecutablePath(),
ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir));
var editorId = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
.GetEditorSettings().GetSetting("mono/editor/external_editor");
string editorIdentity = GetExternalEditorIdentity(editorId);
GodotIdeServer.Logger = Logger;
var runningServer = GetRunningOrNewServer();
GodotIdeServer.StartServer();
}
if (runningServer.IsAnyConnected(editorIdentity))
return new EditorPick(editorIdentity);
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
LaunchIde(editorId, editorIdentity);
var timeoutTask = Task.Delay(millisecondsTimeout);
var completedTask = await Task.WhenAny(timeoutTask, runningServer.AwaitClientConnected(editorIdentity));
if (completedTask != timeoutTask)
return new EditorPick(editorIdentity);
GodotIdeServer?.Dispose();
return null;
}
private void LaunchIde()
private void LaunchIde(ExternalEditorId editorId, string editorIdentity)
{
var editor = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
.GetEditorSettings().GetSetting("mono/editor/external_editor");
switch (editor)
switch (editorId)
{
case ExternalEditorId.None:
case ExternalEditorId.VisualStudio:
......@@ -80,14 +111,14 @@ namespace GodotTools.Ides
{
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);
return vsForMacInstance;
}
monoDevelInstance = monoDevelInstance ??
monoDevelInstance = (monoDevelInstance?.IsDisposed ?? true ? null : monoDevelInstance) ??
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop);
return monoDevelInstance;
}
......@@ -96,12 +127,25 @@ namespace GodotTools.Ides
{
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();
}
}
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}");
}
......@@ -113,26 +157,45 @@ namespace GodotTools.Ides
}
}
private void WriteMessage(string id, params string[] arguments)
public readonly struct EditorPick
{
GetRunningServer().WriteMessage(new Message(id, arguments));
}
private readonly string identity;
public void SendOpenFile(string file)
{
WriteMessage("OpenFile", file);
}
public EditorPick(string identity)
{
this.identity = identity;
}
public void SendOpenFile(string file, int line)
{
WriteMessage("OpenFile", file, line.ToString());
}
public bool IsAnyConnected() =>
GodotSharpEditor.Instance.GodotIdeManager.GetRunningOrNewServer().IsAnyConnected(identity);
public void SendOpenFile(string file, int line, int column)
{
WriteMessage("OpenFile", file, line.ToString(), column.ToString());
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 EditorPick PickEditor(ExternalEditorId editorId) => new EditorPick(GetExternalEditorIdentity(editorId));
private class GodotLogger : ILogger
{
public void LogDebug(string message)
......
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;
}
}
}
}
......@@ -7,14 +7,16 @@ using GodotTools.Utils;
namespace GodotTools.Ides.MonoDevelop
{
public class Instance
public class Instance : IDisposable
{
public DateTime LaunchTime { get; private set; }
private readonly string solutionFile;
private readonly EditorId editorId;
private Process process;
public bool IsRunning => process != null && !process.HasExited;
public bool IsDisposed { get; private set; }
public void Execute()
{
......@@ -59,6 +61,8 @@ namespace GodotTools.Ides.MonoDevelop
if (command == null)
throw new FileNotFoundException();
LaunchTime = DateTime.Now;
if (newWindow)
{
process = Process.Start(new ProcessStartInfo
......@@ -88,6 +92,12 @@ namespace GodotTools.Ides.MonoDevelop
this.editorId = editorId;
}
public void Dispose()
{
IsDisposed = true;
process?.Dispose();
}
private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames;
private static readonly IReadOnlyDictionary<EditorId, string> BundleIds;
......@@ -118,7 +128,7 @@ namespace GodotTools.Ides.MonoDevelop
{EditorId.MonoDevelop, "MonoDevelop.exe"}
};
}
else if (OS.IsUnixLike())
else if (OS.IsUnixLike)
{
ExecutableNames = new Dictionary<EditorId, string>
{
......
......@@ -36,7 +36,7 @@ namespace GodotTools.Ides.Rider
{
return CollectRiderInfosMac();
}
if (OS.IsUnixLike())
if (OS.IsUnixLike)
{
return CollectAllRiderPathsLinux();
}
......@@ -141,16 +141,16 @@ namespace GodotTools.Ides.Rider
if (OS.IsOSX)
{
var home = Environment.GetEnvironmentVariable("HOME");
if (string.IsNullOrEmpty(home))
if (string.IsNullOrEmpty(home))
return string.Empty;
var localAppData = Path.Combine(home, @"Library/Application Support");
return GetToolboxRiderRootPath(localAppData);
}
if (OS.IsUnixLike())
if (OS.IsUnixLike)
{
var home = Environment.GetEnvironmentVariable("HOME");
if (string.IsNullOrEmpty(home))
if (string.IsNullOrEmpty(home))
return string.Empty;
var localAppData = Path.Combine(home, @".local/share");
return GetToolboxRiderRootPath(localAppData);
......@@ -209,7 +209,7 @@ namespace GodotTools.Ides.Rider
private static string GetRelativePathToBuildTxt()
{
if (OS.IsWindows || OS.IsUnixLike())
if (OS.IsWindows || OS.IsUnixLike)
return "../../build.txt";
if (OS.IsOSX)
return "Contents/Resources/build.txt";
......@@ -322,7 +322,7 @@ namespace GodotTools.Ides.Rider
class SettingsJson
{
public string install_location;
[CanBeNull]
public static string GetInstallLocationFromJson(string json)
{
......
......@@ -2,6 +2,7 @@ using System;
using System.Runtime.CompilerServices;
using Godot;
using Godot.Collections;
using GodotTools.IdeMessaging.Requests;
namespace GodotTools.Internals
{
......@@ -52,6 +53,9 @@ namespace GodotTools.Internals
public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts();
public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) =>
internal_CodeCompletionRequest((int)kind, scriptFile);
#region Internal
[MethodImpl(MethodImplOptions.InternalCall)]
......@@ -111,6 +115,9 @@ namespace GodotTools.Internals
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_ScriptEditorDebugger_ReloadScripts();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string[] internal_CodeCompletionRequest(int kind, string scriptFile);
#endregion
}
}
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;
}
}
}
using System.Reflection;
using System.Runtime.CompilerServices;
// Information about this assembly is defined by the following attributes.
// Change them to the values specific to your project.
[assembly: AssemblyTitle("GodotTools")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("")]
[assembly: AssemblyCopyright("Godot Engine contributors")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
// The form "{Major}.{Minor}.*" will automatically update the build and revision,
// and "{Major}.{Minor}.{Build}.*" will update just the revision.
[assembly: AssemblyVersion("1.0.*")]
// The following attributes are used to specify the signing key for the assembly,
// if desired. See the Mono documentation for more information about signing.
//[assembly: AssemblyDelaySign(false)]
//[assembly: AssemblyKeyFile("")]
......@@ -22,7 +22,10 @@ namespace GodotTools.Utils
{
public const string Windows = "Windows";
public const string OSX = "OSX";
public const string X11 = "X11";
public const string Linux = "Linux";
public const string FreeBSD = "FreeBSD";
public const string NetBSD = "NetBSD";
public const string BSD = "BSD";
public const string Server = "Server";
public const string UWP = "UWP";
public const string Haiku = "Haiku";
......@@ -35,7 +38,7 @@ namespace GodotTools.Utils
{
public const string Windows = "windows";
public const string OSX = "osx";
public const string X11 = "linuxbsd";
public const string LinuxBSD = "linuxbsd";
public const string Server = "server";
public const string UWP = "uwp";
public const string Haiku = "haiku";
......@@ -48,7 +51,10 @@ namespace GodotTools.Utils
{
[Names.Windows] = Platforms.Windows,
[Names.OSX] = Platforms.OSX,
[Names.X11] = Platforms.X11,
[Names.Linux] = Platforms.LinuxBSD,
[Names.FreeBSD] = Platforms.LinuxBSD,
[Names.NetBSD] = Platforms.LinuxBSD,
[Names.BSD] = Platforms.LinuxBSD,
[Names.Server] = Platforms.Server,
[Names.UWP] = Platforms.UWP,
[Names.Haiku] = Platforms.Haiku,
......@@ -62,38 +68,39 @@ namespace GodotTools.Utils
return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
}
private static bool IsAnyOS(IEnumerable<string> names)
{
return names.Any(p => p.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase));
}
private static readonly IEnumerable<string> LinuxBSDPlatforms =
new[] {Names.Linux, Names.FreeBSD, Names.NetBSD, Names.BSD};
private static readonly IEnumerable<string> UnixLikePlatforms =
new[] {Names.OSX, Names.Server, Names.Haiku, Names.Android, Names.iOS}
.Concat(LinuxBSDPlatforms).ToArray();
private static readonly Lazy<bool> _isWindows = new Lazy<bool>(() => IsOS(Names.Windows));
private static readonly Lazy<bool> _isOSX = new Lazy<bool>(() => IsOS(Names.OSX));
private static readonly Lazy<bool> _isX11 = new Lazy<bool>(() => IsOS(Names.X11));
private static readonly Lazy<bool> _isLinuxBSD = new Lazy<bool>(() => IsAnyOS(LinuxBSDPlatforms));
private static readonly Lazy<bool> _isServer = new Lazy<bool>(() => IsOS(Names.Server));
private static readonly Lazy<bool> _isUWP = new Lazy<bool>(() => IsOS(Names.UWP));
private static readonly Lazy<bool> _isHaiku = new Lazy<bool>(() => IsOS(Names.Haiku));
private static readonly Lazy<bool> _isAndroid = new Lazy<bool>(() => IsOS(Names.Android));
private static readonly Lazy<bool> _isiOS = new Lazy<bool>(() => IsOS(Names.iOS));
private static readonly Lazy<bool> _isHTML5 = new Lazy<bool>(() => IsOS(Names.HTML5));
private static readonly Lazy<bool> _isUnixLike = new Lazy<bool>(() => IsAnyOS(UnixLikePlatforms));
public static bool IsWindows => _isWindows.Value || IsUWP;
public static bool IsOSX => _isOSX.Value;
public static bool IsX11 => _isX11.Value;
public static bool IsLinuxBSD => _isLinuxBSD.Value;
public static bool IsServer => _isServer.Value;
public static bool IsUWP => _isUWP.Value;
public static bool IsHaiku => _isHaiku.Value;
public static bool IsAndroid => _isAndroid.Value;
public static bool IsiOS => _isiOS.Value;
public static bool IsHTML5 => _isHTML5.Value;
private static bool? _isUnixCache;
private static readonly string[] UnixLikePlatforms = { Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android, Names.iOS };
public static bool IsUnixLike()
{
if (_isUnixCache.HasValue)
return _isUnixCache.Value;
string osName = GetPlatformName();
_isUnixCache = UnixLikePlatforms.Any(p => p.Equals(osName, StringComparison.OrdinalIgnoreCase));
return _isUnixCache.Value;
}
public static bool IsUnixLike => _isUnixLike.Value;
public static char PathSep => IsWindows ? ';' : ':';
......@@ -121,10 +128,10 @@ namespace GodotTools.Utils
return searchDirs.Select(dir => Path.Combine(dir, name)).FirstOrDefault(File.Exists);
return (from dir in searchDirs
select Path.Combine(dir, name)
select Path.Combine(dir, name)
into path
from ext in windowsExts
select path + ext).FirstOrDefault(File.Exists);
from ext in windowsExts
select path + ext).FirstOrDefault(File.Exists);
}
private static string PathWhichUnix([NotNull] string name)
......@@ -189,7 +196,7 @@ namespace GodotTools.Utils
startInfo.UseShellExecute = false;
using (var process = new Process { StartInfo = startInfo })
using (var process = new Process {StartInfo = startInfo})
{
process.Start();
process.WaitForExit();
......
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="JetBrains.Annotations" version="2019.1.3" targetFramework="net45" />
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net45" />
</packages>
......@@ -1664,6 +1664,10 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf
}
if (!p_imethod.is_internal) {
// TODO: This alone adds ~0.2 MB of bloat to the core API assembly. It would be
// better to generate a table in the C++ glue instead. That way the strings wouldn't
// add that much extra bloat as they're already used in engine code. Also, it would
// probably be much faster than looking up the attributes when fetching methods.
p_output.append(MEMBER_BEGIN "[GodotMethod(\"");
p_output.append(p_imethod.name);
p_output.append("\")]");
......@@ -2139,7 +2143,7 @@ Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInte
if (return_type->ret_as_byref_arg) {
p_output.append("\tif (" CS_PARAM_INSTANCE " == nullptr) { *arg_ret = ");
p_output.append(fail_ret);
p_output.append("; ERR_FAIL_MSG(\"Parameter ' arg_ret ' is null.\"); }\n");
p_output.append("; ERR_FAIL_MSG(\"Parameter ' " CS_PARAM_INSTANCE " ' is null.\"); }\n");
} else {
p_output.append("\tERR_FAIL_NULL_V(" CS_PARAM_INSTANCE ", ");
p_output.append(fail_ret);
......@@ -2390,6 +2394,11 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY)
continue;
if (property.name.find("/") >= 0) {
// Ignore properties with '/' (slash) in the name. These are only meant for use in the inspector.
continue;
}
PropertyInterface iprop;
iprop.cname = property.name;
iprop.setter = ClassDB::get_property_setter(type_cname, iprop.cname);
......@@ -2402,7 +2411,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
bool valid = false;
iprop.index = ClassDB::get_property_index(type_cname, iprop.cname, &valid);
ERR_FAIL_COND_V(!valid, false);
ERR_FAIL_COND_V_MSG(!valid, false, "Invalid property: '" + itype.name + "." + String(iprop.cname) + "'.");
iprop.proxy_name = escape_csharp_keyword(snake_to_pascal_case(iprop.cname));
......@@ -2414,8 +2423,6 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
iprop.proxy_name += "_";
}
iprop.proxy_name = iprop.proxy_name.replace("/", "__"); // Some members have a slash...
iprop.prop_doc = nullptr;
for (int i = 0; i < itype.class_doc->properties.size(); i++) {
......
/*************************************************************************/
/* 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, PackedStringArray &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, PackedStringArray &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;
}
PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_script_file) {
PackedStringArray 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
/*************************************************************************/
/* 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
};
PackedStringArray 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 "../utils/osx_utils.h"
#include "bindings_generator.h"
#include "code_completion.h"
#include "godotsharp_export.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);
PackedStringArray suggestions = gdmono::get_code_completion((gdmono::CompletionKind)p_kind, script_file);
return GDMonoMarshal::PackedStringArray_to_mono_array(suggestions);
}
float godot_icall_Globals_EditorScale() {
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_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_CodeCompletionRequest", (void *)godot_icall_Internal_CodeCompletionRequest);
// Globals
mono_add_internal_call("GodotTools.Internals.Globals::internal_EditorScale", (void *)godot_icall_Globals_EditorScale);
......
......@@ -32,68 +32,70 @@
#include <mono/metadata/image.h>
#include "core/io/file_access_pack.h"
#include "core/os/os.h"
#include "core/project_settings.h"
#include "../mono_gd/gd_mono.h"
#include "../mono_gd/gd_mono_assembly.h"
#include "../mono_gd/gd_mono_cache.h"
#include "../utils/macros.h"
namespace GodotSharpExport {
String get_assemblyref_name(MonoImage *p_image, int index) {
struct AssemblyRefInfo {
String name;
uint16_t major;
uint16_t minor;
uint16_t build;
uint16_t revision;
};
AssemblyRefInfo get_assemblyref_name(MonoImage *p_image, int index) {
const MonoTableInfo *table_info = mono_image_get_table_info(p_image, MONO_TABLE_ASSEMBLYREF);
uint32_t cols[MONO_ASSEMBLYREF_SIZE];
mono_metadata_decode_row(table_info, index, cols, MONO_ASSEMBLYREF_SIZE);
return String::utf8(mono_metadata_string_heap(p_image, cols[MONO_ASSEMBLYREF_NAME]));
return {
String::utf8(mono_metadata_string_heap(p_image, cols[MONO_ASSEMBLYREF_NAME])),
(uint16_t)cols[MONO_ASSEMBLYREF_MAJOR_VERSION],
(uint16_t)cols[MONO_ASSEMBLYREF_MINOR_VERSION],
(uint16_t)cols[MONO_ASSEMBLYREF_BUILD_NUMBER],
(uint16_t)cols[MONO_ASSEMBLYREF_REV_NUMBER]
};
}
Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String> &p_search_dirs, Dictionary &r_assembly_dependencies) {
MonoImage *image = p_assembly->get_image();
for (int i = 0; i < mono_image_get_table_rows(image, MONO_TABLE_ASSEMBLYREF); i++) {
String ref_name = get_assemblyref_name(image, i);
AssemblyRefInfo ref_info = get_assemblyref_name(image, i);
const String &ref_name = ref_info.name;
if (r_assembly_dependencies.has(ref_name))
continue;
GDMonoAssembly *ref_assembly = nullptr;
String path;
bool has_extension = ref_name.ends_with(".dll") || ref_name.ends_with(".exe");
for (int j = 0; j < p_search_dirs.size(); j++) {
const String &search_dir = p_search_dirs[j];
if (has_extension) {
path = search_dir.plus_file(ref_name);
if (FileAccess::exists(path)) {
GDMono::get_singleton()->load_assembly_from(ref_name.get_basename(), path, &ref_assembly, true);
if (ref_assembly != nullptr)
break;
}
} else {
path = search_dir.plus_file(ref_name + ".dll");
if (FileAccess::exists(path)) {
GDMono::get_singleton()->load_assembly_from(ref_name, path, &ref_assembly, true);
if (ref_assembly != nullptr)
break;
}
path = search_dir.plus_file(ref_name + ".exe");
if (FileAccess::exists(path)) {
GDMono::get_singleton()->load_assembly_from(ref_name, path, &ref_assembly, true);
if (ref_assembly != nullptr)
break;
}
}
}
GDMonoAssembly *ref_assembly = NULL;
{
MonoAssemblyName *ref_aname = mono_assembly_name_new("A"); // We can't allocate an empty MonoAssemblyName, hence "A"
CRASH_COND(ref_aname == nullptr);
SCOPE_EXIT {
mono_assembly_name_free(ref_aname);
mono_free(ref_aname);
};
ERR_FAIL_COND_V_MSG(!ref_assembly, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'.");
mono_assembly_get_assemblyref(image, i, ref_aname);
// Use the path we got from the search. Don't try to get the path from the loaded assembly as we can't trust it will be from the selected BCL dir.
r_assembly_dependencies[ref_name] = path;
if (!GDMono::get_singleton()->load_assembly(ref_name, ref_aname, &ref_assembly, /* refonly: */ true, p_search_dirs)) {
ERR_FAIL_V_MSG(ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'.");
}
r_assembly_dependencies[ref_name] = ref_assembly->get_path();
}
Error err = get_assembly_dependencies(ref_assembly, p_search_dirs, r_assembly_dependencies);
ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot load one of the dependencies for the assembly: '" + ref_name + "'.");
......@@ -113,6 +115,11 @@ Error get_exported_assembly_dependencies(const Dictionary &p_initial_assemblies,
Vector<String> search_dirs;
GDMonoAssembly::fill_search_dirs(search_dirs, p_build_config, p_custom_bcl_dir);
if (p_custom_bcl_dir.length()) {
// Only one mscorlib can be loaded. We need this workaround to make sure we get it from the right BCL directory.
r_assembly_dependencies["mscorlib"] = p_custom_bcl_dir.plus_file("mscorlib.dll").simplify_path();
}
for (const Variant *key = p_initial_assemblies.next(); key; key = p_initial_assemblies.next(key)) {
String assembly_name = *key;
String assembly_path = p_initial_assemblies[*key];
......
......@@ -82,6 +82,7 @@ CallableCustom::CompareLessFunc ManagedCallable::get_compare_less_func() const {
}
ObjectID ManagedCallable::get_object() const {
// TODO: If the delegate target extends Godot.Object, use that instead!
return CSharpLanguage::get_singleton()->get_managed_callable_middleman()->get_instance_id();
}
......
......@@ -133,6 +133,10 @@ void gd_mono_debug_init() {
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
int da_port = GLOBAL_DEF("mono/debugger_agent/port", 23685);
bool da_suspend = GLOBAL_DEF("mono/debugger_agent/wait_for_debugger", false);
......@@ -515,8 +519,8 @@ void GDMono::add_assembly(uint32_t p_domain_id, GDMonoAssembly *p_assembly) {
GDMonoAssembly *GDMono::get_loaded_assembly(const String &p_name) {
if (p_name == "mscorlib")
return get_corlib_assembly();
if (p_name == "mscorlib" && corlib_assembly)
return corlib_assembly;
MonoDomain *domain = mono_domain_get();
uint32_t domain_id = domain ? mono_domain_get_id(domain) : 0;
......@@ -526,7 +530,9 @@ GDMonoAssembly *GDMono::get_loaded_assembly(const String &p_name) {
bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly) {
#ifdef DEBUG_ENABLED
CRASH_COND(!r_assembly);
#endif
MonoAssemblyName *aname = mono_assembly_name_new(p_name.utf8());
bool result = load_assembly(p_name, aname, r_assembly, p_refonly);
......@@ -538,26 +544,27 @@ bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bo
bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly) {
#ifdef DEBUG_ENABLED
CRASH_COND(!r_assembly);
#endif
print_verbose("Mono: Loading assembly " + p_name + (p_refonly ? " (refonly)" : "") + "...");
return load_assembly(p_name, p_aname, r_assembly, p_refonly, GDMonoAssembly::get_default_search_dirs());
}
MonoImageOpenStatus status = MONO_IMAGE_OK;
MonoAssembly *assembly = mono_assembly_load_full(p_aname, nullptr, &status, p_refonly);
bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly, const Vector<String> &p_search_dirs) {
if (!assembly)
return false;
ERR_FAIL_COND_V(status != MONO_IMAGE_OK, false);
#ifdef DEBUG_ENABLED
CRASH_COND(!r_assembly);
#endif
uint32_t domain_id = mono_domain_get_id(mono_domain_get());
print_verbose("Mono: Loading assembly " + p_name + (p_refonly ? " (refonly)" : "") + "...");
GDMonoAssembly **stored_assembly = assemblies[domain_id].getptr(p_name);
GDMonoAssembly *assembly = GDMonoAssembly::load(p_name, p_aname, p_refonly, p_search_dirs);
ERR_FAIL_COND_V(stored_assembly == nullptr, false);
ERR_FAIL_COND_V((*stored_assembly)->get_assembly() != assembly, false);
if (!assembly)
return false;
*r_assembly = *stored_assembly;
*r_assembly = assembly;
print_verbose("Mono: Assembly " + p_name + (p_refonly ? " (refonly)" : "") + " loaded from path: " + (*r_assembly)->get_path());
......
......@@ -241,6 +241,7 @@ public:
bool load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly = false);
bool load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly = false);
bool load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly, const Vector<String> &p_search_dirs);
bool load_assembly_from(const String &p_name, const String &p_path, GDMonoAssembly **r_assembly, bool p_refonly = false);
Error finalize_and_unload_domain(MonoDomain *p_domain);
......
......@@ -33,6 +33,7 @@
#include <mono/metadata/mono-debug.h>
#include <mono/metadata/tokentype.h>
#include "core/io/file_access_pack.h"
#include "core/list.h"
#include "core/os/file_access.h"
#include "core/os/os.h"
......@@ -99,7 +100,7 @@ void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const Strin
// - The 'load' hook is called after the assembly has been loaded. Its job is to add the
// assembly to the list of loaded assemblies so that the 'search' hook can look it up.
void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, void *user_data) {
void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, [[maybe_unused]] void *user_data) {
String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly)));
......@@ -133,9 +134,7 @@ MonoAssembly *GDMonoAssembly::assembly_refonly_preload_hook(MonoAssemblyName *an
return GDMonoAssembly::_preload_hook(aname, assemblies_path, user_data, true);
}
MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly) {
(void)user_data; // UNUSED
MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, [[maybe_unused]] void *user_data, bool refonly) {
String name = String::utf8(mono_assembly_name_get_name(aname));
bool has_extension = name.ends_with(".dll") || name.ends_with(".exe");
......@@ -147,15 +146,13 @@ MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, void *user_d
return nullptr;
}
MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, void *user_data, bool refonly) {
(void)user_data; // UNUSED
MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, [[maybe_unused]] void *user_data, bool refonly) {
String name = String::utf8(mono_assembly_name_get_name(aname));
return _load_assembly_search(name, search_dirs, refonly);
return _load_assembly_search(name, aname, refonly, search_dirs);
}
MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly) {
MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) {
MonoAssembly *res = nullptr;
String path;
......@@ -168,21 +165,21 @@ MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const
if (has_extension) {
path = search_dir.plus_file(p_name);
if (FileAccess::exists(path)) {
res = _real_load_assembly_from(path, p_refonly);
res = _real_load_assembly_from(path, p_refonly, p_aname);
if (res != nullptr)
return res;
}
} else {
path = search_dir.plus_file(p_name + ".dll");
if (FileAccess::exists(path)) {
res = _real_load_assembly_from(path, p_refonly);
res = _real_load_assembly_from(path, p_refonly, p_aname);
if (res != nullptr)
return res;
}
path = search_dir.plus_file(p_name + ".exe");
if (FileAccess::exists(path)) {
res = _real_load_assembly_from(path, p_refonly);
res = _real_load_assembly_from(path, p_refonly, p_aname);
if (res != nullptr)
return res;
}
......@@ -230,7 +227,7 @@ void GDMonoAssembly::initialize() {
mono_install_assembly_load_hook(&assembly_load_hook, nullptr);
}
MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly) {
MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly, MonoAssemblyName *p_aname) {
Vector<uint8_t> data = FileAccess::get_file_as_array(p_path);
ERR_FAIL_COND_V_MSG(data.empty(), nullptr, "Could read the assembly in the specified location");
......@@ -255,7 +252,33 @@ MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, boo
true, &status, p_refonly,
image_filename.utf8());
ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, nullptr, "Failed to open assembly image from the loaded data");
ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, nullptr, "Failed to open assembly image from memory: '" + p_path + "'.");
if (p_aname != nullptr) {
// Check assembly version
const MonoTableInfo *table = mono_image_get_table_info(image, MONO_TABLE_ASSEMBLY);
ERR_FAIL_NULL_V(table, nullptr);
if (mono_table_info_get_rows(table)) {
uint32_t cols[MONO_ASSEMBLY_SIZE];
mono_metadata_decode_row(table, 0, cols, MONO_ASSEMBLY_SIZE);
// Not sure about .NET's policy. We will only ensure major and minor are equal, and ignore build and revision.
uint16_t major = cols[MONO_ASSEMBLY_MAJOR_VERSION];
uint16_t minor = cols[MONO_ASSEMBLY_MINOR_VERSION];
uint16_t required_minor;
uint16_t required_major = mono_assembly_name_get_version(p_aname, &required_minor, nullptr, nullptr);
if (required_major != 0) {
if (major != required_major && minor != required_minor) {
mono_image_close(image);
return nullptr;
}
}
}
}
#ifdef DEBUG_ENABLED
Vector<uint8_t> pdb_data;
......@@ -425,6 +448,26 @@ GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class)
return match;
}
GDMonoAssembly *GDMonoAssembly::load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) {
if (GDMono::get_singleton()->get_corlib_assembly() && (p_name == "mscorlib" || p_name == "mscorlib.dll"))
return GDMono::get_singleton()->get_corlib_assembly();
// We need to manually call the search hook in this case, as it won't be called in the next step
MonoAssembly *assembly = mono_assembly_invoke_search_hook(p_aname);
if (!assembly) {
assembly = _load_assembly_search(p_name, p_aname, p_refonly, p_search_dirs);
ERR_FAIL_NULL_V(assembly, nullptr);
}
GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(p_name);
ERR_FAIL_NULL_V_MSG(loaded_asm, nullptr, "Loaded assembly missing from table. Did we not receive the load hook?");
ERR_FAIL_COND_V(loaded_asm->get_assembly() != assembly, nullptr);
return loaded_asm;
}
GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_path, bool p_refonly) {
if (p_name == "mscorlib" || p_name == "mscorlib.dll")
......
......@@ -93,8 +93,8 @@ class GDMonoAssembly {
static MonoAssembly *_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly);
static MonoAssembly *_preload_hook(MonoAssemblyName *aname, char **assemblies_path, void *user_data, bool refonly);
static MonoAssembly *_real_load_assembly_from(const String &p_path, bool p_refonly);
static MonoAssembly *_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly);
static MonoAssembly *_real_load_assembly_from(const String &p_path, bool p_refonly, MonoAssemblyName *p_aname = nullptr);
static MonoAssembly *_load_assembly_search(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs);
friend class GDMono;
static void initialize();
......@@ -120,7 +120,9 @@ public:
static String find_assembly(const String &p_name);
static void fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config = String(), const String &p_custom_bcl_dir = String());
static const Vector<String> &get_default_search_dirs() { return search_dirs; }
static GDMonoAssembly *load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs);
static GDMonoAssembly *load_from(const String &p_name, const String &p_path, bool p_refonly);
GDMonoAssembly(const String &p_name, MonoImage *p_image, MonoAssembly *p_assembly);
......
......@@ -175,7 +175,7 @@ void GDMonoLog::initialize() {
log_level_id = get_log_level_id(log_level.get_data());
if (log_file) {
OS::get_singleton()->print("Mono: Logfile is: %s\n", log_file_path.utf8().get_data());
OS::get_singleton()->print("Mono: Log file is: '%s'\n", log_file_path.utf8().get_data());
mono_trace_set_log_handler(mono_log_callback, this);
} else {
OS::get_singleton()->printerr("Mono: No log file, using default log handler\n");
......
......@@ -68,6 +68,6 @@ public:
} // namespace gdmono
#define SCOPE_EXIT \
auto GD_UNIQUE_NAME(gd_scope_exit) = gdmono::ScopeExitAux() + [=]()
auto GD_UNIQUE_NAME(gd_scope_exit) = gdmono::ScopeExitAux() + [=]() -> void
#endif // UTIL_MACROS_H
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment