From af4acb5b1163089fe727000b39cefb4fc799f1db Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Sat, 9 May 2020 20:54:07 +0200 Subject: C#/Mono: Check assembly version when loading Not sure if we should check revision too, but this is good enough for what we want. This will be needed to load the correct Microsoft.Build when we switch to the nuget version. --- modules/mono/editor/bindings_generator.cpp | 15 ++++-- modules/mono/editor/godotsharp_export.cpp | 79 ++++++++++++++++-------------- 2 files changed, 54 insertions(+), 40 deletions(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index bdf9cf965f..258b8ed3ed 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -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++) { diff --git a/modules/mono/editor/godotsharp_export.cpp b/modules/mono/editor/godotsharp_export.cpp index 4126da16be..d6a271f1d9 100644 --- a/modules/mono/editor/godotsharp_export.cpp +++ b/modules/mono/editor/godotsharp_export.cpp @@ -32,68 +32,70 @@ #include +#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 &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 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]; -- cgit v1.2.3 From f3bcd5f8dd36c4ce384f00a03650f71dce5c4de6 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Sat, 9 May 2020 20:55:50 +0200 Subject: C#: Use Sdks in GodotTools csprojs and switch to nuget Microsoft.Build --- .../GodotTools.BuildLogger/GodotBuildLogger.cs | 14 +++- .../GodotTools.BuildLogger.csproj | 58 +------------ .../Properties/AssemblyInfo.cs | 35 -------- .../GodotTools.Core/GodotTools.Core.csproj | 39 +-------- .../GodotTools.Core/Properties/AssemblyInfo.cs | 26 ------ .../GodotTools/GodotTools.Core/StringExtensions.cs | 14 +--- .../GodotTools.IdeConnection.csproj | 50 +---------- .../Properties/AssemblyInfo.cs | 35 -------- .../GodotTools.ProjectEditor.csproj | 64 ++++---------- .../GodotTools.ProjectEditor/ProjectExtensions.cs | 11 +-- .../GodotTools.ProjectEditor/ProjectUtils.cs | 7 +- .../Properties/AssemblyInfo.cs | 27 ------ .../GodotTools.ProjectEditor/packages.config | 4 - .../GodotTools/GodotTools/Build/BuildSystem.cs | 4 +- .../GodotTools/GodotTools/Build/MsBuildFinder.cs | 50 +++++------ .../editor/GodotTools/GodotTools/BuildManager.cs | 14 ++-- .../mono/editor/GodotTools/GodotTools/BuildTab.cs | 12 +-- .../GodotTools/GodotTools/Export/AotBuilder.cs | 2 +- .../GodotTools/GodotTools/Export/ExportPlugin.cs | 2 +- .../GodotTools/GodotTools/GodotSharpEditor.cs | 10 +-- .../editor/GodotTools/GodotTools/GodotTools.csproj | 97 +++------------------- .../GodotTools/Ides/MonoDevelop/Instance.cs | 2 +- .../GodotTools/Ides/Rider/RiderPathLocator.cs | 12 +-- .../GodotTools/Properties/AssemblyInfo.cs | 26 ------ .../mono/editor/GodotTools/GodotTools/Utils/OS.cs | 51 +++++++----- .../editor/GodotTools/GodotTools/packages.config | 5 -- 26 files changed, 140 insertions(+), 531 deletions(-) delete mode 100644 modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config delete mode 100644 modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools/packages.config (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs index 6015cb22b6..c2549b4ad5 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs @@ -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; + } + } } diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj index 8fdd485209..0afec970c6 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj @@ -1,60 +1,10 @@ - - - + - Debug - AnyCPU {6CE9A984-37B1-4F8A-8FE9-609F05F071B3} - Library - Properties - GodotTools.BuildLogger - GodotTools.BuildLogger - v4.7 - 512 - 7 + netstandard2.0 + 7.2 - - AnyCPU - true - portable - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - portable - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - {639e48bd-44e5-4091-8edd-22d36dc0768d} - GodotTools.Core - + - - diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs deleted file mode 100644 index 4374f21cfa..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("GodotTools.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")] diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj index c9ea7d3a2c..d6d8962f90 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj @@ -1,40 +1,7 @@ - - + - Debug - AnyCPU {639E48BD-44E5-4091-8EDD-22D36DC0768D} - Library - GodotTools.Core - GodotTools.Core - v4.7 - 7 + netstandard2.0 + 7.2 - - true - full - false - bin\Debug - DEBUG; - prompt - 4 - false - - - true - bin\Release - prompt - 4 - false - - - - - - - - - - - diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index 699ae6e741..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -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("")] diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs index 326c49f096..7ab5c5fc59 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs @@ -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) diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj index 8454535fba..c3ce3654ed 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj @@ -1,53 +1,11 @@ - - - + - Debug - AnyCPU {92600954-25F0-4291-8E11-1FEE9FC4BE20} Library - Properties - GodotTools.IdeConnection - GodotTools.IdeConnection - v4.7 - 512 - 7 + net472 + 7.2 - - AnyCPU - true - portable - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - portable - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - + - diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs deleted file mode 100644 index 0806d02ca0..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("GodotTools.IdeConnection")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("Godot Engine contributors")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("92600954-25F0-4291-8E11-1FEE9FC4BE20")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index b60e501beb..3bb136e374 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -1,57 +1,23 @@ - - + - Debug - AnyCPU {A8CDAD94-C6D4-4B19-A7E7-76C53CC92984} - Library - GodotTools.ProjectEditor - GodotTools.ProjectEditor - v4.7 - obj - 7 + net472 + 7.2 - - true - portable - false - bin\Debug - DEBUG; - prompt - 4 - false - - - true - bin\Release - prompt - 4 - false - - - - - - $(SolutionDir)\packages\DotNet.Glob.2.1.1\lib\net45\DotNet.Glob.dll - - - - - - - - - + + - + - - - {639E48BD-44E5-4091-8EDD-22D36DC0768D} - GodotTools.Core - - - + + + $([System.String]::Copy('$(AssemblySearchPaths)').Replace('{CandidateAssemblyFiles}', '')) + $(AssemblySearchPaths.Split(';')) + diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs index f0e0d1b33d..704f2ec194 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs @@ -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; diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index f2ebef1a7d..a0356d0f49 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -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(); 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... diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs deleted file mode 100644 index 3a0464c9bc..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -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("")] - diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config deleted file mode 100644 index 2db030f9d8..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 43c96d2e30..f4cda3e522 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -109,9 +109,9 @@ namespace GodotTools.Build buildInfo.LogsDirPath, buildInfo.CustomProperties); } - public static async Task BuildAsync(BuildInfo buildInfo) + public static Task BuildAsync(BuildInfo buildInfo) { - return await BuildAsync(buildInfo.Solution, buildInfo.Configuration, + return BuildAsync(buildInfo.Solution, buildInfo.Configuration, buildInfo.LogsDirPath, buildInfo.CustomProperties); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs index af8d070cbd..73de09c116 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs @@ -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(); 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("\\")) diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs index 520e665595..cd646e6201 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs @@ -15,9 +15,9 @@ namespace GodotTools { private static readonly List BuildsInProgress = new List(); - 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"; @@ -251,9 +251,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 +261,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); diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs index 938c3d8be1..0106e1f1ac 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs @@ -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; diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs index f1765f7e19..f60e469503 100755 --- a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs @@ -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"; diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 2ceb4888a2..6bfbc62f3b 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -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"; diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index c070cb16d9..812d575f3b 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -256,10 +256,9 @@ namespace GodotTools 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); @@ -311,7 +310,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 +320,7 @@ namespace GodotTools } else { - if (_vsCodePath.Empty()) + if (string.IsNullOrEmpty(_vsCodePath)) { GD.PushError("Cannot find code editor: VSCode"); return Error.FileNotFound; @@ -341,7 +340,6 @@ namespace GodotTools break; } - default: throw new ArgumentOutOfRangeException(); } @@ -505,7 +503,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}" + diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index e1570d6465..16e3e948be 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -1,14 +1,8 @@ - - + - Debug - AnyCPU {27B00618-A6F2-4828-B922-05CAEB08C286} - Library - GodotTools - GodotTools - v4.7 - 7 + net472 + 7.2 Debug $(SolutionDir)/../../../../ $(GodotSourceRootPath)/bin/GodotSharp @@ -18,32 +12,12 @@ $(GodotOutputDataDir)/Tools - - - true - portable - false - DEBUG; - prompt - 4 - false - - - true - prompt - 4 - false + + False - - ..\packages\JetBrains.Annotations.2019.1.3\lib\net20\JetBrains.Annotations.dll - True - - - ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll - True - - + + $(GodotApiAssembliesDir)/GodotSharp.dll False @@ -54,58 +28,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {6ce9a984-37b1-4f8a-8fe9-609f05f071b3} - GodotTools.BuildLogger - - - {92600954-25f0-4291-8e11-1fee9fc4be20} - GodotTools.IdeConnection - - - {A8CDAD94-C6D4-4B19-A7E7-76C53CC92984} - GodotTools.ProjectEditor - - - {639E48BD-44E5-4091-8EDD-22D36DC0768D} - GodotTools.Core - - - - + + + + - diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs index 6026c109ad..0fdc964039 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs @@ -118,7 +118,7 @@ namespace GodotTools.Ides.MonoDevelop {EditorId.MonoDevelop, "MonoDevelop.exe"} }; } - else if (OS.IsUnixLike()) + else if (OS.IsUnixLike) { ExecutableNames = new Dictionary { diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs index e3a4fa7b45..e22e9af919 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs @@ -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) { diff --git a/modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs deleted file mode 100644 index f5fe85c722..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -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("")] diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index b057ac12c6..6c05891f2c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -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 names) + { + return names.Any(p => p.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase)); + } + + private static readonly IEnumerable LinuxBSDPlatforms = + new[] {Names.Linux, Names.FreeBSD, Names.NetBSD, Names.BSD}; + + private static readonly IEnumerable UnixLikePlatforms = + new[] {Names.OSX, Names.Server, Names.Haiku, Names.Android, Names.iOS} + .Concat(LinuxBSDPlatforms).ToArray(); + private static readonly Lazy _isWindows = new Lazy(() => IsOS(Names.Windows)); private static readonly Lazy _isOSX = new Lazy(() => IsOS(Names.OSX)); - private static readonly Lazy _isX11 = new Lazy(() => IsOS(Names.X11)); + private static readonly Lazy _isLinuxBSD = new Lazy(() => IsAnyOS(LinuxBSDPlatforms)); private static readonly Lazy _isServer = new Lazy(() => IsOS(Names.Server)); private static readonly Lazy _isUWP = new Lazy(() => IsOS(Names.UWP)); private static readonly Lazy _isHaiku = new Lazy(() => IsOS(Names.Haiku)); private static readonly Lazy _isAndroid = new Lazy(() => IsOS(Names.Android)); private static readonly Lazy _isiOS = new Lazy(() => IsOS(Names.iOS)); private static readonly Lazy _isHTML5 = new Lazy(() => IsOS(Names.HTML5)); + private static readonly Lazy _isUnixLike = new Lazy(() => 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(); diff --git a/modules/mono/editor/GodotTools/GodotTools/packages.config b/modules/mono/editor/GodotTools/GodotTools/packages.config deleted file mode 100644 index dd3de2865a..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - -- cgit v1.2.3 From 3ce09246d10a267bf00ae8bf37068ec458c69287 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Sat, 9 May 2020 20:45:43 +0200 Subject: C#: Rewrite GodotTools messaging protocol --- .../GodotTools.IdeConnection/ConsoleLogger.cs | 33 -- .../GodotTools.IdeConnection/GodotIdeBase.cs | 94 ------ .../GodotTools.IdeConnection/GodotIdeClient.cs | 219 ------------- .../GodotTools.IdeConnection/GodotIdeConnection.cs | 207 ------------ .../GodotIdeConnectionClient.cs | 24 -- .../GodotIdeConnectionServer.cs | 24 -- .../GodotTools.IdeConnection/GodotIdeMetadata.cs | 45 --- .../GodotTools.IdeConnection.csproj | 11 - .../GodotTools/GodotTools.IdeConnection/ILogger.cs | 13 - .../GodotTools/GodotTools.IdeConnection/Message.cs | 21 -- .../GodotTools.IdeConnection/MessageComposer.cs | 46 --- .../GodotTools.IdeConnection/MessageParser.cs | 88 ----- .../ForwarderMessageHandler.cs | 57 ++++ .../GodotTools.IdeMessaging.CLI.csproj | 17 + .../GodotTools.IdeMessaging.CLI/Program.cs | 218 +++++++++++++ .../GodotTools/GodotTools.IdeMessaging/Client.cs | 332 +++++++++++++++++++ .../GodotTools.IdeMessaging/ClientHandshake.cs | 44 +++ .../ClientMessageHandler.cs | 52 +++ .../GodotTools.IdeMessaging/GodotIdeMetadata.cs | 47 +++ .../GodotTools.IdeMessaging.csproj | 24 ++ .../GodotTools.IdeMessaging/IHandshake.cs | 8 + .../GodotTools/GodotTools.IdeMessaging/ILogger.cs | 13 + .../GodotTools.IdeMessaging/IMessageHandler.cs | 9 + .../GodotTools/GodotTools.IdeMessaging/Message.cs | 52 +++ .../GodotTools.IdeMessaging/MessageDecoder.cs | 100 ++++++ .../GodotTools/GodotTools.IdeMessaging/Peer.cs | 302 +++++++++++++++++ .../GodotTools.IdeMessaging/Requests/Requests.cs | 116 +++++++ .../GodotTools.IdeMessaging/ResponseAwaiter.cs | 23 ++ .../GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs | 64 ++++ .../Utils/SemaphoreExtensions.cs | 32 ++ modules/mono/editor/GodotTools/GodotTools.sln | 2 +- .../editor/GodotTools/GodotTools/BuildManager.cs | 5 +- .../GodotTools/GodotTools/GodotSharpEditor.cs | 22 +- .../editor/GodotTools/GodotTools/GodotTools.csproj | 2 +- .../GodotTools/GodotTools/Ides/GodotIdeManager.cs | 163 +++++++--- .../GodotTools/GodotTools/Ides/GodotIdeServer.cs | 212 ------------ .../GodotTools/GodotTools/Ides/MessagingServer.cs | 360 +++++++++++++++++++++ .../GodotTools/Ides/MonoDevelop/Instance.cs | 12 +- .../GodotTools/GodotTools/Internals/Internal.cs | 7 + .../editor/GodotTools/GodotTools/PlaySettings.cs | 19 ++ .../GodotTools/GodotTools/Utils/NotifyAwaiter.cs | 64 ---- modules/mono/editor/code_completion.cpp | 249 ++++++++++++++ modules/mono/editor/code_completion.h | 56 ++++ modules/mono/editor/editor_internal_calls.cpp | 8 + 44 files changed, 2352 insertions(+), 1164 deletions(-) delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs create mode 100644 modules/mono/editor/code_completion.cpp create mode 100644 modules/mono/editor/code_completion.h (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs deleted file mode 100644 index 7a2ff2ca56..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace GodotTools.IdeConnection -{ - public class ConsoleLogger : ILogger - { - public void LogDebug(string message) - { - Console.WriteLine("DEBUG: " + message); - } - - public void LogInfo(string message) - { - Console.WriteLine("INFO: " + message); - } - - public void LogWarning(string message) - { - Console.WriteLine("WARN: " + message); - } - - public void LogError(string message) - { - Console.WriteLine("ERROR: " + message); - } - - public void LogError(string message, Exception e) - { - Console.WriteLine("EXCEPTION: " + message); - Console.WriteLine(e); - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs deleted file mode 100644 index be89638241..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using Path = System.IO.Path; - -namespace GodotTools.IdeConnection -{ - public class GodotIdeBase : IDisposable - { - private ILogger logger; - - public ILogger Logger - { - get => logger ?? (logger = new ConsoleLogger()); - set => logger = value; - } - - private readonly string projectMetadataDir; - - protected const string MetaFileName = "ide_server_meta.txt"; - protected string MetaFilePath => Path.Combine(projectMetadataDir, MetaFileName); - - private GodotIdeConnection connection; - protected readonly object ConnectionLock = new object(); - - public bool IsDisposed { get; private set; } = false; - - public bool IsConnected => connection != null && !connection.IsDisposed && connection.IsConnected; - - public event Action Connected - { - add - { - if (connection != null && !connection.IsDisposed) - connection.Connected += value; - } - remove - { - if (connection != null && !connection.IsDisposed) - connection.Connected -= value; - } - } - - protected GodotIdeConnection Connection - { - get => connection; - set - { - connection?.Dispose(); - connection = value; - } - } - - protected GodotIdeBase(string projectMetadataDir) - { - this.projectMetadataDir = projectMetadataDir; - } - - protected void DisposeConnection() - { - lock (ConnectionLock) - { - connection?.Dispose(); - } - } - - ~GodotIdeBase() - { - Dispose(disposing: false); - } - - public void Dispose() - { - if (IsDisposed) - return; - - lock (ConnectionLock) - { - if (IsDisposed) // lock may not be fair - return; - IsDisposed = true; - } - - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - connection?.Dispose(); - } - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs deleted file mode 100644 index 2bf3b83c75..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Threading; - -namespace GodotTools.IdeConnection -{ - public abstract class GodotIdeClient : GodotIdeBase - { - protected GodotIdeMetadata GodotIdeMetadata; - - private readonly FileSystemWatcher fsWatcher; - - protected GodotIdeClient(string projectMetadataDir) : base(projectMetadataDir) - { - messageHandlers = InitializeMessageHandlers(); - - // FileSystemWatcher requires an existing directory - if (!File.Exists(projectMetadataDir)) - Directory.CreateDirectory(projectMetadataDir); - - fsWatcher = new FileSystemWatcher(projectMetadataDir, MetaFileName); - } - - private void OnMetaFileChanged(object sender, FileSystemEventArgs e) - { - if (IsDisposed) - return; - - lock (ConnectionLock) - { - if (IsDisposed) - return; - - if (!File.Exists(MetaFilePath)) - return; - - var metadata = ReadMetadataFile(); - - if (metadata != null && metadata != GodotIdeMetadata) - { - GodotIdeMetadata = metadata.Value; - ConnectToServer(); - } - } - } - - private void OnMetaFileDeleted(object sender, FileSystemEventArgs e) - { - if (IsDisposed) - return; - - if (IsConnected) - DisposeConnection(); - - // The file may have been re-created - - lock (ConnectionLock) - { - if (IsDisposed) - return; - - if (IsConnected || !File.Exists(MetaFilePath)) - return; - - var metadata = ReadMetadataFile(); - - if (metadata != null) - { - GodotIdeMetadata = metadata.Value; - ConnectToServer(); - } - } - } - - private GodotIdeMetadata? ReadMetadataFile() - { - using (var reader = File.OpenText(MetaFilePath)) - { - string portStr = reader.ReadLine(); - - if (portStr == null) - return null; - - string editorExecutablePath = reader.ReadLine(); - - if (editorExecutablePath == null) - return null; - - if (!int.TryParse(portStr, out int port)) - return null; - - return new GodotIdeMetadata(port, editorExecutablePath); - } - } - - private void ConnectToServer() - { - var tcpClient = new TcpClient(); - - Connection = new GodotIdeConnectionClient(tcpClient, HandleMessage); - Connection.Logger = Logger; - - try - { - Logger.LogInfo("Connecting to Godot Ide Server"); - - tcpClient.Connect(IPAddress.Loopback, GodotIdeMetadata.Port); - - Logger.LogInfo("Connection open with Godot Ide Server"); - - var clientThread = new Thread(Connection.Start) - { - IsBackground = true, - Name = "Godot Ide Connection Client" - }; - clientThread.Start(); - } - catch (SocketException e) - { - if (e.SocketErrorCode == SocketError.ConnectionRefused) - Logger.LogError("The connection to the Godot Ide Server was refused"); - else - throw; - } - } - - public void Start() - { - Logger.LogInfo("Starting Godot Ide Client"); - - fsWatcher.Changed += OnMetaFileChanged; - fsWatcher.Deleted += OnMetaFileDeleted; - fsWatcher.EnableRaisingEvents = true; - - lock (ConnectionLock) - { - if (IsDisposed) - return; - - if (!File.Exists(MetaFilePath)) - { - Logger.LogInfo("There is no Godot Ide Server running"); - return; - } - - var metadata = ReadMetadataFile(); - - if (metadata != null) - { - GodotIdeMetadata = metadata.Value; - ConnectToServer(); - } - else - { - Logger.LogError("Failed to read Godot Ide metadata file"); - } - } - } - - public bool WriteMessage(Message message) - { - return Connection.WriteMessage(message); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - fsWatcher?.Dispose(); - } - } - - protected virtual bool HandleMessage(Message message) - { - if (messageHandlers.TryGetValue(message.Id, out var action)) - { - action(message.Arguments); - return true; - } - - return false; - } - - private readonly Dictionary> messageHandlers; - - private Dictionary> InitializeMessageHandlers() - { - return new Dictionary> - { - ["OpenFile"] = args => - { - switch (args.Length) - { - case 1: - OpenFile(file: args[0]); - return; - case 2: - OpenFile(file: args[0], line: int.Parse(args[1])); - return; - case 3: - OpenFile(file: args[0], line: int.Parse(args[1]), column: int.Parse(args[2])); - return; - default: - throw new ArgumentException(); - } - } - }; - } - - protected abstract void OpenFile(string file); - protected abstract void OpenFile(string file, int line); - protected abstract void OpenFile(string file, int line, int column); - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs deleted file mode 100644 index 6441be8d6e..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Net.Sockets; -using System.Text; - -namespace GodotTools.IdeConnection -{ - public abstract class GodotIdeConnection : IDisposable - { - protected const string Version = "1.0"; - - protected static readonly string ClientHandshake = $"Godot Ide Client Version {Version}"; - protected static readonly string ServerHandshake = $"Godot Ide Server Version {Version}"; - - private const int ClientWriteTimeout = 8000; - private readonly TcpClient tcpClient; - - private TextReader clientReader; - private TextWriter clientWriter; - - private readonly object writeLock = new object(); - - private readonly Func 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 messageHandler) - { - this.tcpClient = tcpClient; - this.messageHandler = messageHandler; - } - - public void Start() - { - try - { - if (!StartConnection()) - return; - - string messageLine; - while ((messageLine = ReadLine()) != null) - { - if (!MessageParser.TryParse(messageLine, out Message msg)) - { - Logger.LogError($"Received message with invalid format: {messageLine}"); - continue; - } - - Logger.LogDebug($"Received message: {msg}"); - - if (msg.Id == "close") - { - Logger.LogInfo("Closing connection"); - return; - } - - try - { - try - { - Debug.Assert(messageHandler != null); - - if (!messageHandler(msg)) - Logger.LogError($"Received unknown message: {msg}"); - } - catch (Exception e) - { - Logger.LogError($"Message handler for '{msg}' failed with exception", e); - } - } - catch (Exception e) - { - Logger.LogError($"Exception thrown from message handler. Message: {msg}", e); - } - } - } - catch (Exception e) - { - Logger.LogError($"Unhandled exception in the Godot Ide Connection thread", e); - } - finally - { - Dispose(); - } - } - - private bool StartConnection() - { - NetworkStream clientStream = tcpClient.GetStream(); - - clientReader = new StreamReader(clientStream, Encoding.UTF8); - - lock (writeLock) - clientWriter = new StreamWriter(clientStream, Encoding.UTF8); - - clientStream.WriteTimeout = ClientWriteTimeout; - - if (!WriteHandshake()) - { - Logger.LogError("Could not write handshake"); - return false; - } - - if (!IsValidResponseHandshake(ReadLine())) - { - Logger.LogError("Received invalid handshake"); - return false; - } - - Connected?.Invoke(); - - Logger.LogInfo("Godot Ide connection started"); - - return true; - } - - private string ReadLine() - { - try - { - return clientReader?.ReadLine(); - } - catch (Exception e) - { - if (IsDisposed) - { - var se = e as SocketException ?? e.InnerException as SocketException; - if (se != null && se.SocketErrorCode == SocketError.Interrupted) - return null; - } - - throw; - } - } - - public bool WriteMessage(Message message) - { - Logger.LogDebug($"Sending message {message}"); - - var messageComposer = new MessageComposer(); - - messageComposer.AddArgument(message.Id); - foreach (string argument in message.Arguments) - messageComposer.AddArgument(argument); - - return WriteLine(messageComposer.ToString()); - } - - protected bool WriteLine(string text) - { - if (clientWriter == null || IsDisposed || !IsConnected) - return false; - - lock (writeLock) - { - try - { - clientWriter.WriteLine(text); - clientWriter.Flush(); - } - catch (Exception e) - { - if (!IsDisposed) - { - var se = e as SocketException ?? e.InnerException as SocketException; - if (se != null && se.SocketErrorCode == SocketError.Shutdown) - Logger.LogInfo("Client disconnected ungracefully"); - else - Logger.LogError("Exception thrown when trying to write to client", e); - - Dispose(); - } - } - } - - return true; - } - - protected abstract bool WriteHandshake(); - protected abstract bool IsValidResponseHandshake(string handshakeLine); - - public void Dispose() - { - if (IsDisposed) - return; - - IsDisposed = true; - - clientReader?.Dispose(); - clientWriter?.Dispose(); - ((IDisposable)tcpClient)?.Dispose(); - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs deleted file mode 100644 index 1b11a14358..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Net.Sockets; -using System.Threading.Tasks; - -namespace GodotTools.IdeConnection -{ - public class GodotIdeConnectionClient : GodotIdeConnection - { - public GodotIdeConnectionClient(TcpClient tcpClient, Func messageHandler) - : base(tcpClient, messageHandler) - { - } - - protected override bool WriteHandshake() - { - return WriteLine(ClientHandshake); - } - - protected override bool IsValidResponseHandshake(string handshakeLine) - { - return handshakeLine == ServerHandshake; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs deleted file mode 100644 index aa98dc7ca3..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Net.Sockets; -using System.Threading.Tasks; - -namespace GodotTools.IdeConnection -{ - public class GodotIdeConnectionServer : GodotIdeConnection - { - public GodotIdeConnectionServer(TcpClient tcpClient, Func messageHandler) - : base(tcpClient, messageHandler) - { - } - - protected override bool WriteHandshake() - { - return WriteLine(ServerHandshake); - } - - protected override bool IsValidResponseHandshake(string handshakeLine) - { - return handshakeLine == ClientHandshake; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs deleted file mode 100644 index d16daba0e2..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace GodotTools.IdeConnection -{ - public struct GodotIdeMetadata - { - public int Port { get; } - public string EditorExecutablePath { get; } - - public GodotIdeMetadata(int port, string editorExecutablePath) - { - Port = port; - EditorExecutablePath = editorExecutablePath; - } - - public static bool operator ==(GodotIdeMetadata a, GodotIdeMetadata b) - { - return a.Port == b.Port && a.EditorExecutablePath == b.EditorExecutablePath; - } - - public static bool operator !=(GodotIdeMetadata a, GodotIdeMetadata b) - { - return !(a == b); - } - - public override bool Equals(object obj) - { - if (obj is GodotIdeMetadata metadata) - return metadata == this; - - return false; - } - - public bool Equals(GodotIdeMetadata other) - { - return Port == other.Port && EditorExecutablePath == other.EditorExecutablePath; - } - - public override int GetHashCode() - { - unchecked - { - return (Port * 397) ^ (EditorExecutablePath != null ? EditorExecutablePath.GetHashCode() : 0); - } - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj deleted file mode 100644 index c3ce3654ed..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - {92600954-25F0-4291-8E11-1FEE9FC4BE20} - Library - net472 - 7.2 - - - - - diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs deleted file mode 100644 index 614bb30271..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace GodotTools.IdeConnection -{ - public interface ILogger - { - void LogDebug(string message); - void LogInfo(string message); - void LogWarning(string message); - void LogError(string message); - void LogError(string message, Exception e); - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs deleted file mode 100644 index f24d324ae3..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Linq; - -namespace GodotTools.IdeConnection -{ - public struct Message - { - public string Id { get; set; } - public string[] Arguments { get; set; } - - public Message(string id, params string[] arguments) - { - Id = id; - Arguments = arguments; - } - - public override string ToString() - { - return $"(Id: '{Id}', Arguments: '{string.Join(",", Arguments)}')"; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs deleted file mode 100644 index 30ffe7a06e..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Linq; -using System.Text; - -namespace GodotTools.IdeConnection -{ - public class MessageComposer - { - private readonly StringBuilder stringBuilder = new StringBuilder(); - - private static readonly char[] CharsToEscape = { '\\', '"' }; - - public void AddArgument(string argument) - { - AddArgument(argument, quoted: argument.Contains(",")); - } - - public void AddArgument(string argument, bool quoted) - { - if (stringBuilder.Length > 0) - stringBuilder.Append(','); - - if (quoted) - { - stringBuilder.Append('"'); - - foreach (char @char in argument) - { - if (CharsToEscape.Contains(@char)) - stringBuilder.Append('\\'); - stringBuilder.Append(@char); - } - - stringBuilder.Append('"'); - } - else - { - stringBuilder.Append(argument); - } - } - - public override string ToString() - { - return stringBuilder.ToString(); - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs deleted file mode 100644 index 4365d69989..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace GodotTools.IdeConnection -{ - public static class MessageParser - { - public static bool TryParse(string messageLine, out Message message) - { - var arguments = new List(); - var stringBuilder = new StringBuilder(); - - bool expectingArgument = true; - - for (int i = 0; i < messageLine.Length; i++) - { - char @char = messageLine[i]; - - if (@char == ',') - { - if (expectingArgument) - arguments.Add(string.Empty); - - expectingArgument = true; - continue; - } - - bool quoted = false; - - if (messageLine[i] == '"') - { - quoted = true; - i++; - } - - while (i < messageLine.Length) - { - @char = messageLine[i]; - - if (quoted && @char == '"') - { - i++; - break; - } - - if (@char == '\\') - { - i++; - if (i < messageLine.Length) - break; - - stringBuilder.Append(messageLine[i]); - } - else if (!quoted && @char == ',') - { - break; // We don't increment the counter to allow the colon to be parsed after this - } - else - { - stringBuilder.Append(@char); - } - - i++; - } - - arguments.Add(stringBuilder.ToString()); - stringBuilder.Clear(); - - expectingArgument = false; - } - - if (arguments.Count == 0) - { - message = new Message(); - return false; - } - - message = new Message - { - Id = arguments[0], - Arguments = arguments.Skip(1).ToArray() - }; - - return true; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs new file mode 100644 index 0000000000..3cb6a6687e --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GodotTools.IdeMessaging.Utils; + +namespace GodotTools.IdeMessaging.CLI +{ + public class ForwarderMessageHandler : IMessageHandler + { + private readonly StreamWriter outputWriter; + private readonly SemaphoreSlim outputWriteSem = new SemaphoreSlim(1); + + public ForwarderMessageHandler(StreamWriter outputWriter) + { + this.outputWriter = outputWriter; + } + + public async Task 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} ======="); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj new file mode 100644 index 0000000000..ae78da27bc --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj @@ -0,0 +1,17 @@ + + + {B06C2951-C8E3-4F28-80B2-717CF327EB19} + Exe + net472 + 7.2 + + + + + + + + + + + diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs new file mode 100644 index 0000000000..99a55c471b --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using GodotTools.IdeMessaging.Requests; +using Newtonsoft.Json; + +namespace GodotTools.IdeMessaging.CLI +{ + internal static class Program + { + private static readonly ILogger Logger = new CustomLogger(); + + public static int Main(string[] args) + { + try + { + var mainTask = StartAsync(args, Console.OpenStandardInput(), Console.OpenStandardOutput()); + mainTask.Wait(); + return mainTask.Result; + } + catch (Exception ex) + { + Logger.LogError("Unhandled exception: ", ex); + return 1; + } + } + + private static async Task 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 SendRequest(Client client, string id, MessageContent content) + { + var handlers = new Dictionary>> + { + [PlayRequest.Id] = async () => + { + var request = JsonConvert.DeserializeObject(content.Body); + return await client.SendRequest(request); + }, + [DebugPlayRequest.Id] = async () => + { + var request = JsonConvert.DeserializeObject(content.Body); + return await client.SendRequest(request); + }, + [ReloadScriptsRequest.Id] = async () => + { + var request = JsonConvert.DeserializeObject(content.Body); + return await client.SendRequest(request); + }, + [CodeCompletionRequest.Id] = async () => + { + var request = JsonConvert.DeserializeObject(content.Body); + return await client.SendRequest(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); + } + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs new file mode 100644 index 0000000000..d069651dd3 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using Newtonsoft.Json; +using System.Threading; +using System.Threading.Tasks; +using GodotTools.IdeMessaging.Requests; +using GodotTools.IdeMessaging.Utils; + +namespace GodotTools.IdeMessaging +{ + // ReSharper disable once UnusedType.Global + public sealed class Client : IDisposable + { + private readonly ILogger logger; + + private readonly string identity; + + private string MetaFilePath { get; } + private GodotIdeMetadata godotIdeMetadata; + private readonly FileSystemWatcher fsWatcher; + + private readonly IMessageHandler messageHandler; + + private Peer peer; + private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1); + + private readonly Queue> clientConnectedAwaiters = new Queue>(); + private readonly Queue> clientDisconnectedAwaiters = new Queue>(); + + // ReSharper disable once UnusedMember.Global + public async Task AwaitConnected() + { + var awaiter = new NotifyAwaiter(); + clientConnectedAwaiters.Enqueue(awaiter); + return await awaiter; + } + + // ReSharper disable once UnusedMember.Global + public async Task AwaitDisconnected() + { + var awaiter = new NotifyAwaiter(); + clientDisconnectedAwaiters.Enqueue(awaiter); + return await awaiter; + } + + // ReSharper disable once MemberCanBePrivate.Global + public bool IsDisposed { get; private set; } + + // ReSharper disable once MemberCanBePrivate.Global + public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected; + + // ReSharper disable once EventNeverSubscribedTo.Global + public event Action Connected + { + add + { + if (peer != null && !peer.IsDisposed) + peer.Connected += value; + } + remove + { + if (peer != null && !peer.IsDisposed) + peer.Connected -= value; + } + } + + // ReSharper disable once EventNeverSubscribedTo.Global + public event Action Disconnected + { + add + { + if (peer != null && !peer.IsDisposed) + peer.Disconnected += value; + } + remove + { + if (peer != null && !peer.IsDisposed) + peer.Disconnected -= value; + } + } + + ~Client() + { + Dispose(disposing: false); + } + + public async void Dispose() + { + if (IsDisposed) + return; + + using (await connectionSem.UseAsync()) + { + if (IsDisposed) // lock may not be fair + return; + IsDisposed = true; + } + + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + peer?.Dispose(); + fsWatcher?.Dispose(); + } + } + + public Client(string identity, string godotProjectDir, IMessageHandler messageHandler, ILogger logger) + { + this.identity = identity; + this.messageHandler = messageHandler; + this.logger = logger; + + string projectMetadataDir = Path.Combine(godotProjectDir, ".mono", "metadata"); + + MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName); + + // FileSystemWatcher requires an existing directory + if (!File.Exists(projectMetadataDir)) + Directory.CreateDirectory(projectMetadataDir); + + fsWatcher = new FileSystemWatcher(projectMetadataDir, GodotIdeMetadata.DefaultFileName); + } + + private async void OnMetaFileChanged(object sender, FileSystemEventArgs e) + { + if (IsDisposed) + return; + + using (await connectionSem.UseAsync()) + { + if (IsDisposed) + return; + + if (!File.Exists(MetaFilePath)) + return; + + var metadata = ReadMetadataFile(); + + if (metadata != null && metadata != godotIdeMetadata) + { + godotIdeMetadata = metadata.Value; + _ = Task.Run(ConnectToServer); + } + } + } + + private async void OnMetaFileDeleted(object sender, FileSystemEventArgs e) + { + if (IsDisposed) + return; + + if (IsConnected) + { + using (await connectionSem.UseAsync()) + peer?.Dispose(); + } + + // The file may have been re-created + + using (await connectionSem.UseAsync()) + { + if (IsDisposed) + return; + + if (IsConnected || !File.Exists(MetaFilePath)) + return; + + var metadata = ReadMetadataFile(); + + if (metadata != null) + { + godotIdeMetadata = metadata.Value; + _ = Task.Run(ConnectToServer); + } + } + } + + private GodotIdeMetadata? ReadMetadataFile() + { + using (var reader = File.OpenText(MetaFilePath)) + { + string portStr = reader.ReadLine(); + + if (portStr == null) + return null; + + string editorExecutablePath = reader.ReadLine(); + + if (editorExecutablePath == null) + return null; + + if (!int.TryParse(portStr, out int port)) + return null; + + return new GodotIdeMetadata(port, editorExecutablePath); + } + } + + private async Task AcceptClient(TcpClient tcpClient) + { + logger.LogDebug("Accept client..."); + + using (peer = new Peer(tcpClient, new ClientHandshake(), messageHandler, logger)) + { + // ReSharper disable AccessToDisposedClosure + peer.Connected += () => + { + logger.LogInfo("Connection open with Ide Client"); + + while (clientConnectedAwaiters.Count > 0) + clientConnectedAwaiters.Dequeue().SetResult(true); + }; + + peer.Disconnected += () => + { + while (clientDisconnectedAwaiters.Count > 0) + clientDisconnectedAwaiters.Dequeue().SetResult(true); + }; + // ReSharper restore AccessToDisposedClosure + + try + { + if (!await peer.DoHandshake(identity)) + { + logger.LogError("Handshake failed"); + return; + } + } + catch (Exception e) + { + logger.LogError("Handshake failed with unhandled exception: ", e); + return; + } + + await peer.Process(); + + logger.LogInfo("Connection closed with Ide Client"); + } + } + + private async Task ConnectToServer() + { + var tcpClient = new TcpClient(); + + try + { + logger.LogInfo("Connecting to Godot Ide Server"); + + await tcpClient.ConnectAsync(IPAddress.Loopback, godotIdeMetadata.Port); + + logger.LogInfo("Connection open with Godot Ide Server"); + + await AcceptClient(tcpClient); + } + catch (SocketException e) + { + if (e.SocketErrorCode == SocketError.ConnectionRefused) + logger.LogError("The connection to the Godot Ide Server was refused"); + else + throw; + } + } + + // ReSharper disable once UnusedMember.Global + public async void Start() + { + fsWatcher.Changed += OnMetaFileChanged; + fsWatcher.Deleted += OnMetaFileDeleted; + fsWatcher.EnableRaisingEvents = true; + + using (await connectionSem.UseAsync()) + { + if (IsDisposed) + return; + + if (IsConnected) + return; + + if (!File.Exists(MetaFilePath)) + { + logger.LogInfo("There is no Godot Ide Server running"); + return; + } + + var metadata = ReadMetadataFile(); + + if (metadata != null) + { + godotIdeMetadata = metadata.Value; + _ = Task.Run(ConnectToServer); + } + else + { + logger.LogError("Failed to read Godot Ide metadata file"); + } + } + } + + public async Task SendRequest(Request request) + where TResponse : Response, new() + { + if (!IsConnected) + { + logger.LogError("Cannot write request. Not connected to the Godot Ide Server."); + return null; + } + + string body = JsonConvert.SerializeObject(request); + return await peer.SendRequest(request.Id, body); + } + + public async Task SendRequest(string id, string body) + where TResponse : Response, new() + { + if (!IsConnected) + { + logger.LogError("Cannot write request. Not connected to the Godot Ide Server."); + return null; + } + + return await peer.SendRequest(id, body); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs new file mode 100644 index 0000000000..43041be7be --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs @@ -0,0 +1,44 @@ +using System.Text.RegularExpressions; + +namespace GodotTools.IdeMessaging +{ + public class ClientHandshake : IHandshake + { + private static readonly string ClientHandshakeBase = $"{Peer.ClientHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}"; + private static readonly string ServerHandshakePattern = $@"{Regex.Escape(Peer.ServerHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})"; + + public string GetHandshakeLine(string identity) => $"{ClientHandshakeBase},{identity}"; + + public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger) + { + identity = null; + + var match = Regex.Match(handshake, ServerHandshakePattern); + + if (!match.Success) + return false; + + if (!uint.TryParse(match.Groups[1].Value, out uint serverMajor) || Peer.ProtocolVersionMajor != serverMajor) + { + logger.LogDebug("Incompatible major version: " + match.Groups[1].Value); + return false; + } + + if (!uint.TryParse(match.Groups[2].Value, out uint serverMinor) || Peer.ProtocolVersionMinor < serverMinor) + { + logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value); + return false; + } + + if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision + { + logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value); + return false; + } + + identity = match.Groups[4].Value; + + return true; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs new file mode 100644 index 0000000000..64bcfd824c --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GodotTools.IdeMessaging.Requests; +using Newtonsoft.Json; + +namespace GodotTools.IdeMessaging +{ + // ReSharper disable once UnusedType.Global + public abstract class ClientMessageHandler : IMessageHandler + { + private readonly Dictionary requestHandlers; + + protected ClientMessageHandler() + { + requestHandlers = InitializeRequestHandlers(); + } + + public async Task 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 InitializeRequestHandlers() + { + return new Dictionary + { + [OpenFileRequest.Id] = async (peer, content) => + { + var request = JsonConvert.DeserializeObject(content.Body); + return await HandleOpenFile(request); + } + }; + } + + protected abstract Task HandleOpenFile(OpenFileRequest request); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs new file mode 100644 index 0000000000..686202e81e --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs @@ -0,0 +1,47 @@ +namespace GodotTools.IdeMessaging +{ + public readonly struct GodotIdeMetadata + { + public int Port { get; } + public string EditorExecutablePath { get; } + + public const string DefaultFileName = "ide_messaging_meta.txt"; + + public GodotIdeMetadata(int port, string editorExecutablePath) + { + Port = port; + EditorExecutablePath = editorExecutablePath; + } + + public static bool operator ==(GodotIdeMetadata a, GodotIdeMetadata b) + { + return a.Port == b.Port && a.EditorExecutablePath == b.EditorExecutablePath; + } + + public static bool operator !=(GodotIdeMetadata a, GodotIdeMetadata b) + { + return !(a == b); + } + + public override bool Equals(object obj) + { + if (obj is GodotIdeMetadata metadata) + return metadata == this; + + return false; + } + + public bool Equals(GodotIdeMetadata other) + { + return Port == other.Port && EditorExecutablePath == other.EditorExecutablePath; + } + + public override int GetHashCode() + { + unchecked + { + return (Port * 397) ^ (EditorExecutablePath != null ? EditorExecutablePath.GetHashCode() : 0); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj new file mode 100644 index 0000000000..67815959a6 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj @@ -0,0 +1,24 @@ + + + {92600954-25F0-4291-8E11-1FEE9FC4BE20} + netstandard2.0 + 7.2 + GodotTools.IdeMessaging + 1.1.0 + $(Version) + Godot Engine contributors + + godot + https://github.com/godotengine/godot/tree/master/modules/mono/editor/GodotTools/GodotTools.IdeMessaging + MIT + +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. + + + + + + diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs new file mode 100644 index 0000000000..6387145a28 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs @@ -0,0 +1,8 @@ +namespace GodotTools.IdeMessaging +{ + public interface IHandshake + { + string GetHandshakeLine(string identity); + bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs new file mode 100644 index 0000000000..d2855f93a1 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs @@ -0,0 +1,13 @@ +using System; + +namespace GodotTools.IdeMessaging +{ + public interface ILogger + { + void LogDebug(string message); + void LogInfo(string message); + void LogWarning(string message); + void LogError(string message); + void LogError(string message, Exception e); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs new file mode 100644 index 0000000000..9622fcc96d --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace GodotTools.IdeMessaging +{ + public interface IMessageHandler + { + Task HandleRequest(Peer peer, string id, MessageContent content, ILogger logger); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs new file mode 100644 index 0000000000..6903ec197b --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs @@ -0,0 +1,52 @@ +namespace GodotTools.IdeMessaging +{ + public class Message + { + public MessageKind Kind { get; } + public string Id { get; } + public MessageContent Content { get; } + + public Message(MessageKind kind, string id, MessageContent content) + { + Kind = kind; + Id = id; + Content = content; + } + + public override string ToString() + { + return $"{Kind} | {Id}"; + } + } + + public enum MessageKind + { + Request, + Response + } + + public enum MessageStatus + { + Ok, + RequestNotSupported, + InvalidRequestBody + } + + public readonly struct MessageContent + { + public MessageStatus Status { get; } + public string Body { get; } + + public MessageContent(string body) + { + Status = MessageStatus.Ok; + Body = body; + } + + public MessageContent(MessageStatus status, string body) + { + Status = status; + Body = body; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs new file mode 100644 index 0000000000..a00575a2a1 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs @@ -0,0 +1,100 @@ +using System; +using System.Text; + +namespace GodotTools.IdeMessaging +{ + public class MessageDecoder + { + private class DecodedMessage + { + public MessageKind? Kind; + public string Id; + public MessageStatus? Status; + public readonly StringBuilder Body = new StringBuilder(); + public uint? PendingBodyLines; + + public void Clear() + { + Kind = null; + Id = null; + Status = null; + Body.Clear(); + PendingBodyLines = null; + } + + public Message ToMessage() + { + if (!Kind.HasValue || Id == null || !Status.HasValue || + !PendingBodyLines.HasValue || PendingBodyLines.Value > 0) + throw new InvalidOperationException(); + + return new Message(Kind.Value, Id, new MessageContent(Status.Value, Body.ToString())); + } + } + + public enum State + { + Decoding, + Decoded, + Errored + } + + private readonly DecodedMessage decodingMessage = new DecodedMessage(); + + public State Decode(string messageLine, out Message decodedMessage) + { + decodedMessage = null; + + if (!decodingMessage.Kind.HasValue) + { + if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageKind kind)) + { + decodingMessage.Clear(); + return State.Errored; + } + + decodingMessage.Kind = kind; + } + else if (decodingMessage.Id == null) + { + decodingMessage.Id = messageLine; + } + else if (decodingMessage.Status == null) + { + if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageStatus status)) + { + decodingMessage.Clear(); + return State.Errored; + } + + decodingMessage.Status = status; + } + else if (decodingMessage.PendingBodyLines == null) + { + if (!uint.TryParse(messageLine, out uint pendingBodyLines)) + { + decodingMessage.Clear(); + return State.Errored; + } + + decodingMessage.PendingBodyLines = pendingBodyLines; + } + else + { + if (decodingMessage.PendingBodyLines > 0) + { + decodingMessage.Body.AppendLine(messageLine); + decodingMessage.PendingBodyLines -= 1; + } + else + { + decodedMessage = decodingMessage.ToMessage(); + decodingMessage.Clear(); + return State.Decoded; + } + } + + return State.Decoding; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs new file mode 100644 index 0000000000..a4e86d6177 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GodotTools.IdeMessaging.Requests; +using GodotTools.IdeMessaging.Utils; + +namespace GodotTools.IdeMessaging +{ + public sealed class Peer : IDisposable + { + /// + /// Major version. + /// There is no forward nor backward compatibility between different major versions. + /// Connection is refused if client and server have different major versions. + /// + public static readonly int ProtocolVersionMajor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Major; + + /// + /// Minor version, which clients must be backward compatible with. + /// Connection is refused if the client's minor version is lower than the server's. + /// + public static readonly int ProtocolVersionMinor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Minor; + + /// + /// Revision, which doesn't affect compatibility. + /// + public static readonly int ProtocolVersionRevision = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Revision; + + public const string ClientHandshakeName = "GodotIdeClient"; + public const string ServerHandshakeName = "GodotIdeServer"; + + private const int ClientWriteTimeout = 8000; + + public delegate Task RequestHandler(Peer peer, MessageContent content); + + private readonly TcpClient tcpClient; + + private readonly TextReader clientReader; + private readonly TextWriter clientWriter; + + private readonly SemaphoreSlim writeSem = new SemaphoreSlim(1); + + private string remoteIdentity = string.Empty; + public string RemoteIdentity => remoteIdentity; + + public event Action Connected; + public event Action Disconnected; + + private ILogger Logger { get; } + + public bool IsDisposed { get; private set; } + + public bool IsTcpClientConnected => tcpClient.Client != null && tcpClient.Client.Connected; + + private bool IsConnected { get; set; } + + private readonly IHandshake handshake; + private readonly IMessageHandler messageHandler; + + private readonly Dictionary> requestAwaiterQueues = new Dictionary>(); + private readonly SemaphoreSlim requestsSem = new SemaphoreSlim(1); + + public Peer(TcpClient tcpClient, IHandshake handshake, IMessageHandler messageHandler, ILogger logger) + { + this.tcpClient = tcpClient; + this.handshake = handshake; + this.messageHandler = messageHandler; + + Logger = logger; + + NetworkStream clientStream = tcpClient.GetStream(); + clientStream.WriteTimeout = ClientWriteTimeout; + + clientReader = new StreamReader(clientStream, Encoding.UTF8); + clientWriter = new StreamWriter(clientStream, Encoding.UTF8) {NewLine = "\n"}; + } + + public async Task Process() + { + try + { + var decoder = new MessageDecoder(); + + string messageLine; + while ((messageLine = await ReadLine()) != null) + { + var state = decoder.Decode(messageLine, out var msg); + + if (state == MessageDecoder.State.Decoding) + continue; // Not finished decoding yet + + if (state == MessageDecoder.State.Errored) + { + Logger.LogError($"Received message line with invalid format: {messageLine}"); + continue; + } + + Logger.LogDebug($"Received message: {msg}"); + + try + { + try + { + if (msg.Kind == MessageKind.Request) + { + var responseContent = await messageHandler.HandleRequest(this, msg.Id, msg.Content, Logger); + await WriteMessage(new Message(MessageKind.Response, msg.Id, responseContent)); + } + else if (msg.Kind == MessageKind.Response) + { + ResponseAwaiter responseAwaiter; + + using (await requestsSem.UseAsync()) + { + if (!requestAwaiterQueues.TryGetValue(msg.Id, out var queue) || queue.Count <= 0) + { + Logger.LogError($"Received unexpected response: {msg.Id}"); + return; + } + + responseAwaiter = queue.Dequeue(); + } + + responseAwaiter.SetResult(msg.Content); + } + else + { + throw new IndexOutOfRangeException($"Invalid message kind {msg.Kind}"); + } + } + catch (Exception e) + { + Logger.LogError($"Message handler for '{msg}' failed with exception", e); + } + } + catch (Exception e) + { + Logger.LogError($"Exception thrown from message handler. Message: {msg}", e); + } + } + } + catch (Exception e) + { + Logger.LogError("Unhandled exception in the peer loop", e); + } + } + + public async Task DoHandshake(string identity) + { + if (!await WriteLine(handshake.GetHandshakeLine(identity))) + { + Logger.LogError("Could not write handshake"); + return false; + } + + var readHandshakeTask = ReadLine(); + + if (await Task.WhenAny(readHandshakeTask, Task.Delay(8000)) != readHandshakeTask) + { + Logger.LogError("Timeout waiting for the client handshake"); + return false; + } + + string peerHandshake = await readHandshakeTask; + + if (handshake == null || !handshake.IsValidPeerHandshake(peerHandshake, out remoteIdentity, Logger)) + { + Logger.LogError("Received invalid handshake: " + peerHandshake); + return false; + } + + IsConnected = true; + Connected?.Invoke(); + + Logger.LogInfo("Peer connection started"); + + return true; + } + + private async Task ReadLine() + { + try + { + return await clientReader.ReadLineAsync(); + } + catch (Exception e) + { + if (IsDisposed) + { + var se = e as SocketException ?? e.InnerException as SocketException; + if (se != null && se.SocketErrorCode == SocketError.Interrupted) + return null; + } + + throw; + } + } + + private Task WriteMessage(Message message) + { + Logger.LogDebug($"Sending message: {message}"); + int bodyLineCount = message.Content.Body.Count(c => c == '\n'); + + bodyLineCount += 1; // Extra line break at the end + + var builder = new StringBuilder(); + + builder.AppendLine(message.Kind.ToString()); + builder.AppendLine(message.Id); + builder.AppendLine(message.Content.Status.ToString()); + builder.AppendLine(bodyLineCount.ToString()); + builder.AppendLine(message.Content.Body); + + return WriteLine(builder.ToString()); + } + + public async Task SendRequest(string id, string body) + where TResponse : Response, new() + { + ResponseAwaiter responseAwaiter; + + using (await requestsSem.UseAsync()) + { + bool written = await WriteMessage(new Message(MessageKind.Request, id, new MessageContent(body))); + + if (!written) + return null; + + if (!requestAwaiterQueues.TryGetValue(id, out var queue)) + { + queue = new Queue(); + requestAwaiterQueues.Add(id, queue); + } + + responseAwaiter = new ResponseAwaiter(); + queue.Enqueue(responseAwaiter); + } + + return (TResponse)await responseAwaiter; + } + + private async Task WriteLine(string text) + { + if (clientWriter == null || IsDisposed || !IsTcpClientConnected) + return false; + + using (await writeSem.UseAsync()) + { + try + { + await clientWriter.WriteLineAsync(text); + await clientWriter.FlushAsync(); + } + catch (Exception e) + { + if (!IsDisposed) + { + var se = e as SocketException ?? e.InnerException as SocketException; + if (se != null && se.SocketErrorCode == SocketError.Shutdown) + Logger.LogInfo("Client disconnected ungracefully"); + else + Logger.LogError("Exception thrown when trying to write to client", e); + + Dispose(); + } + } + } + + return true; + } + + // ReSharper disable once UnusedMember.Global + public void ShutdownSocketSend() + { + tcpClient.Client.Shutdown(SocketShutdown.Send); + } + + public void Dispose() + { + if (IsDisposed) + return; + + IsDisposed = true; + + if (IsTcpClientConnected) + { + if (IsConnected) + Disconnected?.Invoke(); + } + + clientReader?.Dispose(); + clientWriter?.Dispose(); + ((IDisposable)tcpClient)?.Dispose(); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs new file mode 100644 index 0000000000..1dd4f852e5 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs @@ -0,0 +1,116 @@ +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +using Newtonsoft.Json; + +namespace GodotTools.IdeMessaging.Requests +{ + public abstract class Request + { + [JsonIgnore] public string Id { get; } + + protected Request(string id) + { + Id = id; + } + } + + public abstract class Response + { + [JsonIgnore] public MessageStatus Status { get; set; } = MessageStatus.Ok; + } + + public sealed class CodeCompletionRequest : Request + { + public enum CompletionKind + { + InputActions = 0, + NodePaths, + ResourcePaths, + ScenePaths, + ShaderParams, + Signals, + ThemeColors, + ThemeConstants, + ThemeFonts, + ThemeStyles + } + + public CompletionKind Kind { get; set; } + public string ScriptFile { get; set; } + + public new const string Id = "CodeCompletion"; + + public CodeCompletionRequest() : base(Id) + { + } + } + + public sealed class CodeCompletionResponse : Response + { + public CodeCompletionRequest.CompletionKind Kind; + public string ScriptFile { get; set; } + public string[] Suggestions { get; set; } + } + + public sealed class PlayRequest : Request + { + public new const string Id = "Play"; + + public PlayRequest() : base(Id) + { + } + } + + public sealed class PlayResponse : Response + { + } + + public sealed class DebugPlayRequest : Request + { + public string DebuggerHost { get; set; } + public int DebuggerPort { get; set; } + public bool? BuildBeforePlaying { get; set; } + + public new const string Id = "DebugPlay"; + + public DebugPlayRequest() : base(Id) + { + } + } + + public sealed class DebugPlayResponse : Response + { + } + + public sealed class OpenFileRequest : Request + { + public string File { get; set; } + public int? Line { get; set; } + public int? Column { get; set; } + + public new const string Id = "OpenFile"; + + public OpenFileRequest() : base(Id) + { + } + } + + public sealed class OpenFileResponse : Response + { + } + + public sealed class ReloadScriptsRequest : Request + { + public new const string Id = "ReloadScripts"; + + public ReloadScriptsRequest() : base(Id) + { + } + } + + public sealed class ReloadScriptsResponse : Response + { + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs new file mode 100644 index 0000000000..548e7f06ee --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs @@ -0,0 +1,23 @@ +using GodotTools.IdeMessaging.Requests; +using GodotTools.IdeMessaging.Utils; +using Newtonsoft.Json; + +namespace GodotTools.IdeMessaging +{ + public abstract class ResponseAwaiter : NotifyAwaiter + { + public abstract void SetResult(MessageContent content); + } + + public class ResponseAwaiter : ResponseAwaiter + where T : Response, new() + { + public override void SetResult(MessageContent content) + { + if (content.Status == MessageStatus.Ok) + SetResult(JsonConvert.DeserializeObject(content.Body)); + else + SetResult(new T {Status = content.Status}); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs new file mode 100644 index 0000000000..d84a63c83c --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.CompilerServices; + +namespace GodotTools.IdeMessaging.Utils +{ + public class NotifyAwaiter : INotifyCompletion + { + private Action continuation; + private Exception exception; + private T result; + + public bool IsCompleted { get; private set; } + + public T GetResult() + { + if (exception != null) + throw exception; + return result; + } + + public void OnCompleted(Action continuation) + { + if (this.continuation != null) + throw new InvalidOperationException("This awaiter has already been listened"); + this.continuation = continuation; + } + + public void SetResult(T result) + { + if (IsCompleted) + throw new InvalidOperationException("This awaiter is already completed"); + + IsCompleted = true; + this.result = result; + + continuation?.Invoke(); + } + + public void SetException(Exception exception) + { + if (IsCompleted) + throw new InvalidOperationException("This awaiter is already completed"); + + IsCompleted = true; + this.exception = exception; + + continuation?.Invoke(); + } + + public NotifyAwaiter Reset() + { + continuation = null; + exception = null; + result = default(T); + IsCompleted = false; + return this; + } + + public NotifyAwaiter GetAwaiter() + { + return this; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs new file mode 100644 index 0000000000..9d593fbf8a --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace GodotTools.IdeMessaging.Utils +{ + public static class SemaphoreExtensions + { + public static ConfiguredTaskAwaitable UseAsync(this SemaphoreSlim semaphoreSlim, CancellationToken cancellationToken = default(CancellationToken)) + { + var wrapper = new SemaphoreSlimWaitReleaseWrapper(semaphoreSlim, out Task waitAsyncTask, cancellationToken); + return waitAsyncTask.ContinueWith(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(); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index a3438ea5f3..f6147eb5bb 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -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 diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs index cd646e6201..2d1b873808 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs @@ -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[] diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 812d575f3b..ec3cae00d5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -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,10 +251,14 @@ 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; } @@ -299,7 +305,7 @@ namespace GodotTools if (line >= 0) { args.Add("-g"); - args.Add($"{scriptPath}:{line + 1}:{col}"); + args.Add($"{scriptPath}:{line}:{col}"); } else { diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 16e3e948be..6857663a4c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -29,7 +29,7 @@ - + diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index 54f0ffab96..e4932ca217 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -1,73 +1,104 @@ 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 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(Request request) + where TResponse : Response, new() + { + // Logs an error if no client is connected with the specified identity + GodotSharpEditor.Instance.GodotIdeManager + .GetRunningOrNewServer() + .BroadcastRequest(identity, request); + } + + public void SendOpenFile(string file) + { + SendRequest(new OpenFileRequest {File = file}); + } + + public void SendOpenFile(string file, int line) + { + SendRequest(new OpenFileRequest {File = file, Line = line}); + } + + public void SendOpenFile(string file, int line, int column) + { + SendRequest(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) diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs deleted file mode 100644 index 72676a8b24..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using GodotTools.IdeConnection; -using GodotTools.Internals; -using GodotTools.Utils; -using Directory = System.IO.Directory; -using File = System.IO.File; -using Thread = System.Threading.Thread; - -namespace GodotTools.Ides -{ - public class GodotIdeServer : GodotIdeBase - { - private readonly TcpListener listener; - private readonly FileStream metaFile; - private readonly Action launchIdeAction; - private readonly NotifyAwaiter clientConnectedAwaiter = new NotifyAwaiter(); - - private async Task 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> messageHandlers; - - private Dictionary> InitializeMessageHandlers() - { - return new Dictionary> - { - ["Play"] = args => - { - switch (args.Length) - { - case 0: - Play(); - return; - case 2: - Play(debuggerHost: args[0], debuggerPort: int.Parse(args[1])); - return; - default: - throw new ArgumentException(); - } - }, - ["ReloadScripts"] = args => ReloadScripts() - }; - } - - private void DispatchToMainThread(Action action) - { - var d = new SendOrPostCallback(state => action()); - Godot.Dispatcher.SynchronizationContext.Post(d, null); - } - - private void Play() - { - DispatchToMainThread(() => - { - CurrentPlayRequest = new PlayRequest(); - Internal.EditorRunPlay(); - CurrentPlayRequest = null; - }); - } - - private void Play(string debuggerHost, int debuggerPort) - { - DispatchToMainThread(() => - { - CurrentPlayRequest = new PlayRequest(debuggerHost, debuggerPort); - Internal.EditorRunPlay(); - CurrentPlayRequest = null; - }); - } - - private void ReloadScripts() - { - DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts); - } - - public PlayRequest? CurrentPlayRequest { get; private set; } - - public struct PlayRequest - { - public bool HasDebugger { get; } - public string DebuggerHost { get; } - public int DebuggerPort { get; } - - public PlayRequest(string debuggerHost, int debuggerPort) - { - HasDebugger = true; - DebuggerHost = debuggerHost; - DebuggerPort = debuggerPort; - } - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs new file mode 100644 index 0000000000..32f264d100 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -0,0 +1,360 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using GodotTools.IdeMessaging; +using GodotTools.IdeMessaging.Requests; +using GodotTools.IdeMessaging.Utils; +using GodotTools.Internals; +using Newtonsoft.Json; +using Directory = System.IO.Directory; +using File = System.IO.File; + +namespace GodotTools.Ides +{ + public sealed class MessagingServer : IDisposable + { + private readonly ILogger logger; + + private readonly FileStream metaFile; + private string MetaFilePath { get; } + + private readonly SemaphoreSlim peersSem = new SemaphoreSlim(1); + + private readonly TcpListener listener; + + private readonly Dictionary>> clientConnectedAwaiters = new Dictionary>>(); + private readonly Dictionary>> clientDisconnectedAwaiters = new Dictionary>>(); + + public async Task AwaitClientConnected(string identity) + { + if (!clientConnectedAwaiters.TryGetValue(identity, out var queue)) + { + queue = new Queue>(); + clientConnectedAwaiters.Add(identity, queue); + } + + var awaiter = new NotifyAwaiter(); + queue.Enqueue(awaiter); + return await awaiter; + } + + public async Task AwaitClientDisconnected(string identity) + { + if (!clientDisconnectedAwaiters.TryGetValue(identity, out var queue)) + { + queue = new Queue>(); + clientDisconnectedAwaiters.Add(identity, queue); + } + + var awaiter = new NotifyAwaiter(); + queue.Enqueue(awaiter); + return await awaiter; + } + + public bool IsDisposed { get; private set; } + + public bool IsAnyConnected(string identity) => string.IsNullOrEmpty(identity) ? + Peers.Count > 0 : + Peers.Any(c => c.RemoteIdentity == identity); + + private List Peers { get; } = new List(); + + ~MessagingServer() + { + Dispose(disposing: false); + } + + public async void Dispose() + { + if (IsDisposed) + return; + + using (await peersSem.UseAsync()) + { + if (IsDisposed) // lock may not be fair + return; + IsDisposed = true; + } + + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + foreach (var connection in Peers) + connection.Dispose(); + Peers.Clear(); + listener?.Stop(); + + metaFile?.Dispose(); + + File.Delete(MetaFilePath); + } + } + + public MessagingServer(string editorExecutablePath, string projectMetadataDir, ILogger logger) + { + this.logger = logger; + + MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName); + + // Make sure the directory exists + Directory.CreateDirectory(projectMetadataDir); + + // The Godot editor's file system thread can keep the file open for writing, so we are forced to allow write sharing... + const FileShare metaFileShare = FileShare.ReadWrite; + + metaFile = File.Open(MetaFilePath, FileMode.Create, FileAccess.Write, metaFileShare); + + listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0)); + listener.Start(); + + int port = ((IPEndPoint)listener.Server.LocalEndPoint).Port; + using (var metaFileWriter = new StreamWriter(metaFile, Encoding.UTF8)) + { + metaFileWriter.WriteLine(port); + metaFileWriter.WriteLine(editorExecutablePath); + } + } + + private async Task AcceptClient(TcpClient tcpClient) + { + logger.LogDebug("Accept client..."); + + using (var peer = new Peer(tcpClient, new ServerHandshake(), new ServerMessageHandler(), logger)) + { + // ReSharper disable AccessToDisposedClosure + peer.Connected += () => + { + logger.LogInfo("Connection open with Ide Client"); + + if (clientConnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue)) + { + while (queue.Count > 0) + queue.Dequeue().SetResult(true); + clientConnectedAwaiters.Remove(peer.RemoteIdentity); + } + }; + + peer.Disconnected += () => + { + if (clientDisconnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue)) + { + while (queue.Count > 0) + queue.Dequeue().SetResult(true); + clientDisconnectedAwaiters.Remove(peer.RemoteIdentity); + } + }; + // ReSharper restore AccessToDisposedClosure + + try + { + if (!await peer.DoHandshake("server")) + { + logger.LogError("Handshake failed"); + return; + } + } + catch (Exception e) + { + logger.LogError("Handshake failed with unhandled exception: ", e); + return; + } + + using (await peersSem.UseAsync()) + Peers.Add(peer); + + try + { + await peer.Process(); + } + finally + { + using (await peersSem.UseAsync()) + Peers.Remove(peer); + } + } + } + + public async Task Listen() + { + try + { + while (!IsDisposed) + _ = AcceptClient(await listener.AcceptTcpClientAsync()); + } + catch (Exception e) + { + if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted)) + throw; + } + } + + public async void BroadcastRequest(string identity, Request request) + where TResponse : Response, new() + { + using (await peersSem.UseAsync()) + { + if (!IsAnyConnected(identity)) + { + logger.LogError("Cannot write request. No client connected to the Godot Ide Server."); + return; + } + + var selectedConnections = string.IsNullOrEmpty(identity) ? + Peers : + Peers.Where(c => c.RemoteIdentity == identity); + + string body = JsonConvert.SerializeObject(request); + + foreach (var connection in selectedConnections) + _ = connection.SendRequest(request.Id, body); + } + } + + private class ServerHandshake : IHandshake + { + private static readonly string ServerHandshakeBase = $"{Peer.ServerHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}"; + private static readonly string ClientHandshakePattern = $@"{Regex.Escape(Peer.ClientHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})"; + + public string GetHandshakeLine(string identity) => $"{ServerHandshakeBase},{identity}"; + + public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger) + { + identity = null; + + var match = Regex.Match(handshake, ClientHandshakePattern); + + if (!match.Success) + return false; + + if (!uint.TryParse(match.Groups[1].Value, out uint clientMajor) || Peer.ProtocolVersionMajor != clientMajor) + { + logger.LogDebug("Incompatible major version: " + match.Groups[1].Value); + return false; + } + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (!uint.TryParse(match.Groups[2].Value, out uint clientMinor) || Peer.ProtocolVersionMinor > clientMinor) + { + logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value); + return false; + } + + if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision + { + logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value); + return false; + } + + identity = match.Groups[4].Value; + + return true; + } + } + + private class ServerMessageHandler : IMessageHandler + { + private static void DispatchToMainThread(Action action) + { + var d = new SendOrPostCallback(state => action()); + Godot.Dispatcher.SynchronizationContext.Post(d, null); + } + + private readonly Dictionary requestHandlers = InitializeRequestHandlers(); + + public async Task HandleRequest(Peer peer, string id, MessageContent content, ILogger logger) + { + if (!requestHandlers.TryGetValue(id, out var handler)) + { + logger.LogError($"Received unknown request: {id}"); + return new MessageContent(MessageStatus.RequestNotSupported, "null"); + } + + try + { + var response = await handler(peer, content); + return new MessageContent(response.Status, JsonConvert.SerializeObject(response)); + } + catch (JsonException) + { + logger.LogError($"Received request with invalid body: {id}"); + return new MessageContent(MessageStatus.InvalidRequestBody, "null"); + } + } + + private static Dictionary InitializeRequestHandlers() + { + return new Dictionary + { + [PlayRequest.Id] = async (peer, content) => + { + _ = JsonConvert.DeserializeObject(content.Body); + return await HandlePlay(); + }, + [DebugPlayRequest.Id] = async (peer, content) => + { + var request = JsonConvert.DeserializeObject(content.Body); + return await HandleDebugPlay(request); + }, + [ReloadScriptsRequest.Id] = async (peer, content) => + { + _ = JsonConvert.DeserializeObject(content.Body); + return await HandleReloadScripts(); + }, + [CodeCompletionRequest.Id] = async (peer, content) => + { + var request = JsonConvert.DeserializeObject(content.Body); + return await HandleCodeCompletionRequest(request); + } + }; + } + + private static Task HandlePlay() + { + DispatchToMainThread(() => + { + GodotSharpEditor.Instance.CurrentPlaySettings = new PlaySettings(); + Internal.EditorRunPlay(); + GodotSharpEditor.Instance.CurrentPlaySettings = null; + }); + return Task.FromResult(new PlayResponse()); + } + + private static Task HandleDebugPlay(DebugPlayRequest request) + { + DispatchToMainThread(() => + { + GodotSharpEditor.Instance.CurrentPlaySettings = + new PlaySettings(request.DebuggerHost, request.DebuggerPort, request.BuildBeforePlaying ?? true); + Internal.EditorRunPlay(); + GodotSharpEditor.Instance.CurrentPlaySettings = null; + }); + return Task.FromResult(new DebugPlayResponse()); + } + + private static Task HandleReloadScripts() + { + DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts); + return Task.FromResult(new ReloadScriptsResponse()); + } + + private static async Task HandleCodeCompletionRequest(CodeCompletionRequest request) + { + var response = new CodeCompletionResponse {Kind = request.Kind, ScriptFile = request.ScriptFile}; + response.Suggestions = await Task.Run(() => Internal.CodeCompletionRequest(response.Kind, response.ScriptFile)); + return response; + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs index 0fdc964039..d6fa2eeba7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs @@ -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 ExecutableNames; private static readonly IReadOnlyDictionary BundleIds; diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 026a7db89c..7e5049e4b7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs @@ -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 } } diff --git a/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs new file mode 100644 index 0000000000..820d0c0b83 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs @@ -0,0 +1,19 @@ +namespace GodotTools +{ + public struct PlaySettings + { + public bool HasDebugger { get; } + public string DebuggerHost { get; } + public int DebuggerPort { get; } + + public bool BuildBeforePlaying { get; } + + public PlaySettings(string debuggerHost, int debuggerPort, bool buildBeforePlaying) + { + HasDebugger = true; + DebuggerHost = debuggerHost; + DebuggerPort = debuggerPort; + BuildBeforePlaying = buildBeforePlaying; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs deleted file mode 100644 index 700b786752..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace GodotTools.Utils -{ - public sealed class NotifyAwaiter : INotifyCompletion - { - private Action continuation; - private Exception exception; - private T result; - - public bool IsCompleted { get; private set; } - - public T GetResult() - { - if (exception != null) - throw exception; - return result; - } - - public void OnCompleted(Action continuation) - { - if (this.continuation != null) - throw new InvalidOperationException("This awaiter has already been listened"); - this.continuation = continuation; - } - - public void SetResult(T result) - { - if (IsCompleted) - throw new InvalidOperationException("This awaiter is already completed"); - - IsCompleted = true; - this.result = result; - - continuation?.Invoke(); - } - - public void SetException(Exception exception) - { - if (IsCompleted) - throw new InvalidOperationException("This awaiter is already completed"); - - IsCompleted = true; - this.exception = exception; - - continuation?.Invoke(); - } - - public NotifyAwaiter Reset() - { - continuation = null; - exception = null; - result = default(T); - IsCompleted = false; - return this; - } - - public NotifyAwaiter GetAwaiter() - { - return this; - } - } -} diff --git a/modules/mono/editor/code_completion.cpp b/modules/mono/editor/code_completion.cpp new file mode 100644 index 0000000000..7a5e465e7a --- /dev/null +++ b/modules/mono/editor/code_completion.cpp @@ -0,0 +1,249 @@ +/*************************************************************************/ +/* code_completion.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "code_completion.h" + +#include "core/project_settings.h" +#include "editor/editor_file_system.h" +#include "editor/editor_settings.h" +#include "scene/gui/control.h" +#include "scene/main/node.h" + +namespace gdmono { + +// Almost everything here is taken from functions used by GDScript for code completion, adapted for C#. + +_FORCE_INLINE_ String quoted(const String &p_str) { + return "\"" + p_str + "\""; +} + +void _add_nodes_suggestions(const Node *p_base, const Node *p_node, 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