From a8eb7ce50964862023bbaabeabf2fa229f9ba951 Mon Sep 17 00:00:00 2001 From: Thad House Date: Thu, 13 Feb 2020 16:31:15 -0800 Subject: [PATCH 01/13] Disallow generating IEquatable for Ref Struct Unlike Equals(object), which discussion seems to prefer generating broken code, generating IEquatable is always invalid This PR makes it so IEquatable can not be generated as a fix for ref structs --- ...ateEqualsAndGetHashCodeFromMembersTests.cs | 30 +++++++++++++++++++ ...hCodeFromMembersCodeRefactoringProvider.cs | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs index 98ebaff1399dd..d5620faf97398 100644 --- a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs +++ b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs @@ -1721,6 +1721,36 @@ public bool Equals(Program other) parameters: CSharp6Implicit); } + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsGenerateEqualsAndGetHashCode)] + public async Task TestImplementIEquatableOnRefStruct() + { + await TestWithPickMembersDialogAsync( +@" +using System.Collections.Generic; + +ref struct Program +{ + public string s; + [||] +}", +@" +using System; +using System.Collections.Generic; + +struct Program : IEquatable +{ + public string s; + + public override bool Equals(object obj) + { + return obj is Program && Equals((Program)obj); + } +}", +chosenSymbols: null, +optionsCallback: options => EnableOption(options, ImplementIEquatableId), +parameters: CSharp6Implicit); + } + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsGenerateEqualsAndGetHashCode)] public async Task TestImplementIEquatableOnStructInNullableContextWithUnannotatedMetadata() { diff --git a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs index abeade7823691..905f3808bf2e3 100644 --- a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs @@ -111,7 +111,7 @@ private async Task HandleNonSelectionAsync(CodeRefactoringContext context) if (equatableTypeOpt != null) { var constructedType = equatableTypeOpt.Construct(containingType); - if (!containingType.AllInterfaces.Contains(constructedType)) + if (!containingType.AllInterfaces.Contains(constructedType) && !containingType.IsRefLikeType) { var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); var value = options.GetOption(GenerateEqualsAndGetHashCodeFromMembersOptions.ImplementIEquatable); From f521455b4aef29fa8fe96fc802a8067f56821ae0 Mon Sep 17 00:00:00 2001 From: Thad House Date: Thu, 13 Feb 2020 16:42:09 -0800 Subject: [PATCH 02/13] Make ref structs generate false --- .../GenerateEqualsAndGetHashCodeFromMembersTests.cs | 4 ++-- .../SyntaxGeneratorExtensions_CreateEqualsMethod.cs | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs index d5620faf97398..bd514a6447f6c 100644 --- a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs +++ b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs @@ -1737,13 +1737,13 @@ ref struct Program using System; using System.Collections.Generic; -struct Program : IEquatable +struct Program { public string s; public override bool Equals(object obj) { - return obj is Program && Equals((Program)obj); + return false; } }", chosenSymbols: null, diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs index 6f9ae9473d9b0..114a600ed1a45 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs @@ -107,6 +107,13 @@ private static ImmutableArray CreateEqualsMethodStatements( { var statements = ArrayBuilder.GetInstance(); + // A ref like type can only return false from equals. + if (containingType.IsRefLikeType) + { + statements.Add(factory.ReturnStatement(factory.FalseLiteralExpression())); + statements.ToImmutableAndFree(); + } + // Come up with a good name for the local variable we're going to compare against. // For example, if the class name is "CustomerOrder" then we'll generate: // From 87893ea8ab6e49b371d393a7641eefffb5f6578a Mon Sep 17 00:00:00 2001 From: Thad House Date: Thu, 13 Feb 2020 16:46:33 -0800 Subject: [PATCH 03/13] Fix missing ref struct --- .../GenerateEqualsAndGetHashCodeFromMembersTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs index bd514a6447f6c..eb42a899779f8 100644 --- a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs +++ b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs @@ -1737,7 +1737,7 @@ ref struct Program using System; using System.Collections.Generic; -struct Program +ref struct Program { public string s; From c5b7a4e2f3f6d91f661420049c1c3b08fabf8156 Mon Sep 17 00:00:00 2001 From: Thad House Date: Thu, 13 Feb 2020 17:04:39 -0800 Subject: [PATCH 04/13] Fix missing return for ToImmutableAndFree() --- .../Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs index 114a600ed1a45..5567ce3e4a45e 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs @@ -111,7 +111,7 @@ private static ImmutableArray CreateEqualsMethodStatements( if (containingType.IsRefLikeType) { statements.Add(factory.ReturnStatement(factory.FalseLiteralExpression())); - statements.ToImmutableAndFree(); + return statements.ToImmutableAndFree(); } // Come up with a good name for the local variable we're going to compare against. From fa878d07c8d38f3900a8c9bd31af87af63f8ea6d Mon Sep 17 00:00:00 2001 From: Thad House Date: Thu, 13 Feb 2020 17:07:43 -0800 Subject: [PATCH 05/13] Run tests, make them actually pass, and fix naming --- ...ateEqualsAndGetHashCodeFromMembersTests.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs index eb42a899779f8..67a56f98a7e7d 100644 --- a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs +++ b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs @@ -1722,21 +1722,40 @@ public bool Equals(Program other) } [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsGenerateEqualsAndGetHashCode)] - public async Task TestImplementIEquatableOnRefStruct() + public async Task TestOverrideEqualsOnRefStructReturnsFalse() { await TestWithPickMembersDialogAsync( @" -using System.Collections.Generic; - ref struct Program { public string s; [||] }", @" -using System; -using System.Collections.Generic; +ref struct Program +{ + public string s; + + public override bool Equals(object obj) + { + return false; + } +}", +chosenSymbols: null, +parameters: CSharp6Implicit); + } + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsGenerateEqualsAndGetHashCode)] + public async Task TestImplementIEquatableOnRefStructSkipsIEquatable() + { + await TestWithPickMembersDialogAsync( +@" +ref struct Program +{ + public string s; + [||] +}", +@" ref struct Program { public string s; From 46d05407325f426319de017b01f4147ef9aa260e Mon Sep 17 00:00:00 2001 From: Thad House Date: Thu, 13 Feb 2020 17:22:02 -0800 Subject: [PATCH 06/13] Fix review comments --- .../GenerateEqualsAndGetHashCodeFromMembersTests.cs | 11 +++++++---- ...ndGetHashCodeFromMembersCodeRefactoringProvider.cs | 2 ++ .../SyntaxGeneratorExtensions_CreateEqualsMethod.cs | 5 ++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs index 67a56f98a7e7d..d53f642ccd032 100644 --- a/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs +++ b/src/EditorFeatures/CSharpTest/GenerateFromMembers/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersTests.cs @@ -1722,6 +1722,7 @@ public bool Equals(Program other) } [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsGenerateEqualsAndGetHashCode)] + [WorkItem(25708, "https://github.com/dotnet/roslyn/issues/25708")] public async Task TestOverrideEqualsOnRefStructReturnsFalse() { await TestWithPickMembersDialogAsync( @@ -1741,11 +1742,11 @@ public override bool Equals(object obj) return false; } }", -chosenSymbols: null, -parameters: CSharp6Implicit); +chosenSymbols: null); } [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsGenerateEqualsAndGetHashCode)] + [WorkItem(25708, "https://github.com/dotnet/roslyn/issues/25708")] public async Task TestImplementIEquatableOnRefStructSkipsIEquatable() { await TestWithPickMembersDialogAsync( @@ -1766,8 +1767,10 @@ public override bool Equals(object obj) } }", chosenSymbols: null, -optionsCallback: options => EnableOption(options, ImplementIEquatableId), -parameters: CSharp6Implicit); +// We are forcefully enabling the ImplementIEquatable option, as that is our way +// to test that the option does nothing. The VS mode will ensure if the option +// is not available it will not be shown. +optionsCallback: options => EnableOption(options, ImplementIEquatableId)); } [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsGenerateEqualsAndGetHashCode)] diff --git a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs index 905f3808bf2e3..4f4ac7934e2d3 100644 --- a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs @@ -111,6 +111,8 @@ private async Task HandleNonSelectionAsync(CodeRefactoringContext context) if (equatableTypeOpt != null) { var constructedType = equatableTypeOpt.Construct(containingType); + // A ref struct can never implement an interface, therefore never add IEquatable to + // the selection options if the type is a ref struct. if (!containingType.AllInterfaces.Contains(constructedType) && !containingType.IsRefLikeType) { var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs index 5567ce3e4a45e..077c192e9a493 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs @@ -107,7 +107,10 @@ private static ImmutableArray CreateEqualsMethodStatements( { var statements = ArrayBuilder.GetInstance(); - // A ref like type can only return false from equals. + // A ref like type can not be boxed. Because of this an overloaded Equals + // taking object in the general case can never be true, because an equivelent + // object can never be boxed into the object itself. Therefore only need to + // return false. if (containingType.IsRefLikeType) { statements.Add(factory.ReturnStatement(factory.FalseLiteralExpression())); From 2d5d2172216c98ef5b506c324f3686b2702aa3be Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 1 May 2020 15:53:55 -0700 Subject: [PATCH 07/13] PR feedback --- ...ashCodeFromMembersCodeRefactoringProvider.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs index 57dd98b462cd4..1e4d093aaeee8 100644 --- a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs @@ -136,14 +136,19 @@ private bool CanImplementIEquatable( SemanticModel semanticModel, INamedTypeSymbol containingType, [NotNullWhen(true)] out INamedTypeSymbol constructedType) { - var equatableTypeOpt = semanticModel.Compilation.GetTypeByMetadataName(typeof(IEquatable<>).FullName); - if (equatableTypeOpt != null) + // A ref struct can never implement an interface, therefore never add IEquatable to the selection + // options if the type is a ref struct. + if (!containingType.IsRefLikeType) { - constructedType = equatableTypeOpt.Construct(containingType); + var equatableTypeOpt = semanticModel.Compilation.GetTypeByMetadataName(typeof(IEquatable<>).FullName); + if (equatableTypeOpt != null) + { + constructedType = equatableTypeOpt.Construct(containingType); - // A ref struct can never implement an interface, therefore never add IEquatable to the selection - // options if the type is a ref struct. - return !containingType.AllInterfaces.Contains(constructedType) && !containingType.IsRefLikeType; + // A ref struct can never implement an interface, therefore never add IEquatable to the selection + // options if the type is a ref struct. + return !containingType.AllInterfaces.Contains(constructedType); + } } constructedType = null; From 2addbcbb3637155b709617dd254c2944148167ef Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 1 May 2020 15:55:43 -0700 Subject: [PATCH 08/13] Enable NRT --- ...hCodeFromMembersCodeRefactoringProvider.cs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs index 1e4d093aaeee8..34e867956e0eb 100644 --- a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#nullable enable + using System; using System.Collections.Immutable; using System.Composition; @@ -35,7 +37,7 @@ internal partial class GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringPro private const string EqualsName = nameof(object.Equals); private const string GetHashCodeName = nameof(object.GetHashCode); - private readonly IPickMembersService _pickMembersService_forTestingPurposes; + private readonly IPickMembersService? _pickMembersService_forTestingPurposes; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] @@ -45,7 +47,7 @@ public GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider() } [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")] - public GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider(IPickMembersService pickMembersService) + public GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider(IPickMembersService? pickMembersService) => _pickMembersService_forTestingPurposes = pickMembersService; public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) @@ -69,9 +71,9 @@ private async Task HandleNonSelectionAsync(CodeRefactoringContext context) { var (document, textSpan, cancellationToken) = context; - var syntaxFacts = document.GetLanguageService(); + var syntaxFacts = document.GetRequiredLanguageService(); var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); // We offer the refactoring when the user is either on the header of a class/struct, // or if they're between any members of a class/struct and are on a blank line. @@ -134,13 +136,13 @@ private bool HasOperator(INamedTypeSymbol containingType, string operatorName) private bool CanImplementIEquatable( SemanticModel semanticModel, INamedTypeSymbol containingType, - [NotNullWhen(true)] out INamedTypeSymbol constructedType) + [NotNullWhen(true)] out INamedTypeSymbol? constructedType) { // A ref struct can never implement an interface, therefore never add IEquatable to the selection // options if the type is a ref struct. if (!containingType.IsRefLikeType) { - var equatableTypeOpt = semanticModel.Compilation.GetTypeByMetadataName(typeof(IEquatable<>).FullName); + var equatableTypeOpt = semanticModel.Compilation.GetTypeByMetadataName(typeof(IEquatable<>).FullName!); if (equatableTypeOpt != null) { constructedType = equatableTypeOpt.Construct(containingType); @@ -182,8 +184,8 @@ public async Task> GenerateEqualsAndGetHashCodeFromMe GetExistingMemberInfo( info.ContainingType, out var hasEquals, out var hasGetHashCode); - var syntaxFacts = document.GetLanguageService(); - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var typeDeclaration = syntaxFacts.GetContainingTypeDeclaration(root, textSpan.Start); return await CreateActionsAsync( @@ -248,15 +250,12 @@ private async Task CreateCodeActionWithDialogAsync( Document document, SyntaxNode typeDeclaration, INamedTypeSymbol containingType, ImmutableArray members, bool generateEquals, bool generateGetHashCode, CancellationToken cancellationToken) { - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); using var _ = ArrayBuilder.GetInstance(out var pickMembersOptions); - var canImplementIEquatable = CanImplementIEquatable(semanticModel, containingType, out var equatableTypeOpt); - var hasExistingOperators = HasOperators(containingType); - - if (canImplementIEquatable) + if (CanImplementIEquatable(semanticModel, containingType, out var equatableTypeOpt)) { var value = options.GetOption(GenerateEqualsAndGetHashCodeFromMembersOptions.ImplementIEquatable); @@ -270,7 +269,7 @@ private async Task CreateCodeActionWithDialogAsync( value)); } - if (!hasExistingOperators) + if (!HasOperators(containingType)) { var value = options.GetOption(GenerateEqualsAndGetHashCodeFromMembersOptions.GenerateOperators); pickMembersOptions.Add(new PickMembersOption( @@ -295,7 +294,7 @@ private async Task CreateCodeActionWithoutDialogAsync( { // if we're generating equals for a struct, then also add IEquatable support as // well as operators (as long as the struct does not already have them). - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); implementIEquatable = CanImplementIEquatable(semanticModel, containingType, out _); generateOperators = !HasOperators(containingType); } From 0037f00d62442ccec9f7013f886b51dce6d5d59e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 1 May 2020 16:04:50 -0700 Subject: [PATCH 09/13] Consistency --- .../SyntaxGeneratorExtensions_CreateEqualsMethod.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs index 2a2db06de7fc6..76df41122d94e 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs @@ -99,7 +99,7 @@ private static ImmutableArray CreateEqualsMethodStatements( ImmutableArray members, string localNameOpt) { - var statements = ArrayBuilder.GetInstance(); + using var _1 = ArrayBuilder.GetInstance(out var statements); // A ref like type can not be boxed. Because of this an overloaded Equals // taking object in the general case can never be true, because an equivelent @@ -108,7 +108,7 @@ private static ImmutableArray CreateEqualsMethodStatements( if (containingType.IsRefLikeType) { statements.Add(factory.ReturnStatement(factory.FalseLiteralExpression())); - return statements.ToImmutableAndFree(); + return statements.ToImmutable(); } // Come up with a good name for the local variable we're going to compare against. @@ -123,7 +123,7 @@ private static ImmutableArray CreateEqualsMethodStatements( // These will be all the expressions that we'll '&&' together inside the final // return statement of 'Equals'. - using var _ = ArrayBuilder.GetInstance(out var expressions); + using var _2 = ArrayBuilder.GetInstance(out var expressions); if (factory.SupportsPatterns(parseOptions)) { @@ -199,7 +199,7 @@ private static ImmutableArray CreateEqualsMethodStatements( statements.Add(factory.ReturnStatement( expressions.Aggregate(factory.LogicalAndExpression))); - return statements.ToImmutableAndFree(); + return statements.ToImmutable(); } private static void AddMemberChecks( From 4f4d5cb7c38c1dc66542531386281babc112123d Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 1 May 2020 16:07:35 -0700 Subject: [PATCH 10/13] Spelling --- .../SyntaxGeneratorExtensions_CreateEqualsMethod.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs index 76df41122d94e..1cf89591c6e1b 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxGeneratorExtensions_CreateEqualsMethod.cs @@ -101,10 +101,9 @@ private static ImmutableArray CreateEqualsMethodStatements( { using var _1 = ArrayBuilder.GetInstance(out var statements); - // A ref like type can not be boxed. Because of this an overloaded Equals - // taking object in the general case can never be true, because an equivelent - // object can never be boxed into the object itself. Therefore only need to - // return false. + // A ref like type can not be boxed. Because of this an overloaded Equals taking object in the general case + // can never be true, because an equivalent object can never be boxed into the object itself. Therefore only + // need to return false. if (containingType.IsRefLikeType) { statements.Add(factory.ReturnStatement(factory.FalseLiteralExpression())); From 44cf2274121cf2c1b48f659e8b3da21470ff8bf7 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 1 May 2020 16:12:34 -0700 Subject: [PATCH 11/13] NRT --- ...ateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs index 34e867956e0eb..102a56c116835 100644 --- a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs @@ -83,7 +83,7 @@ private async Task HandleNonSelectionAsync(CodeRefactoringContext context) return; } - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); // Only supported on classes/structs. var containingType = semanticModel.GetDeclaredSymbol(typeDeclaration) as INamedTypeSymbol; From 7870b4ae4321ea3da5f2b7337350807511aa72e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Matou=C5=A1ek?= Date: Sat, 2 May 2020 16:18:37 -0700 Subject: [PATCH 12/13] Remove Wait (#43752) --- .../Remote/ServiceHubRemoteHostClient.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.cs b/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.cs index 259e191fcd060..d0c6f2c9ed07a 100644 --- a/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.cs +++ b/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.cs @@ -216,21 +216,6 @@ private void UnregisterGlobalOperationNotifications() globalOperationService.Started -= OnGlobalOperationStarted; globalOperationService.Stopped -= OnGlobalOperationStopped; } - - Task localTask; - lock (_globalNotificationsGate) - { - // Unilaterally transition us to the finished state. Once we're finished - // we cannot start or stop anymore. - _globalNotificationsTask = _globalNotificationsTask.ContinueWith( - _ => GlobalNotificationState.Finished, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); - localTask = _globalNotificationsTask; - } - - // Have to wait for all the notifications to make it to the OOP side so we keep - // it in a consistent state. Also, if we don't do this, our _rpc object will - // get disposed while we're remoting over the messages to the oop side. - localTask.Wait(); } private void OnGlobalOperationStarted(object sender, EventArgs e) From 65722f04627de6fab100c714600db68e64c56ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Matou=C5=A1ek?= Date: Sat, 2 May 2020 22:15:13 -0700 Subject: [PATCH 13/13] More RemoteHostClient refactoring (#43928) * Remove direct usage of Connection * Refactor connection pooling * SolutionChecksumUpdater * GlobalNotificationRemoteDeliveryService --- .../Remote/InProcRemostHostClient.cs | 10 +- ...GlobalNotificationRemoteDeliveryService.cs | 145 ++++++++++++ ...tServiceFactory.RemoteHostClientService.cs | 15 +- ...tServiceFactory.SolutionChecksumUpdater.cs | 222 ------------------ ...ceHubRemoteHostClient.ConnectionManager.cs | 88 ++----- ...iceHubRemoteHostClient.PooledConnection.cs | 13 +- .../Remote/ServiceHubRemoteHostClient.cs | 185 ++++----------- .../Remote/SolutionChecksumUpdater.cs | 218 +++++++++++++++++ .../Services/LanguageServiceTests.cs | 2 +- .../Services/ServiceHubServicesTests.cs | 4 +- ...alStudioDiagnosticAnalyzerExecutorTests.cs | 8 +- .../Razor/RazorLanguageServiceClient.cs | 12 +- .../OptionPages/PerformanceLoggersPage.cs | 2 +- .../Pythia/Api/PythiaRemoteHostClient.cs | 18 +- .../Api/UnitTestingRemoteHostClientWrapper.cs | 8 +- .../UnitTestingSessionWithSolutionWrapper.cs | 4 +- ...RemoteGlobalNotificationDeliveryService.cs | 17 ++ .../Portable/Remote/IRemoteHostService.cs | 7 - .../Core/Portable/Remote/RemoteHostClient.cs | 21 +- ...ssionHelpers.cs => SessionWithSolution.cs} | 18 +- .../Remote/WellKnownRemoteHostServices.cs | 14 -- .../Remote/WellKnownServiceHubServices.cs | 2 + ...CodeAnalysisService_GlobalNotifications.cs | 43 ++++ .../ServiceHub/Services/RemoteHostService.cs | 31 --- 24 files changed, 534 insertions(+), 573 deletions(-) create mode 100644 src/VisualStudio/Core/Def/Implementation/Remote/GlobalNotificationRemoteDeliveryService.cs delete mode 100644 src/VisualStudio/Core/Def/Implementation/Remote/RemoteHostClientServiceFactory.SolutionChecksumUpdater.cs create mode 100644 src/VisualStudio/Core/Def/Implementation/Remote/SolutionChecksumUpdater.cs create mode 100644 src/Workspaces/Core/Portable/Remote/IRemoteGlobalNotificationDeliveryService.cs rename src/Workspaces/Core/Portable/Remote/{RemoteHostSessionHelpers.cs => SessionWithSolution.cs} (76%) delete mode 100644 src/Workspaces/Core/Portable/Remote/WellKnownRemoteHostServices.cs create mode 100644 src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_GlobalNotifications.cs diff --git a/src/EditorFeatures/TestUtilities/Remote/InProcRemostHostClient.cs b/src/EditorFeatures/TestUtilities/Remote/InProcRemostHostClient.cs index 2a0f4c9c3edfe..cfb314cc0cc7a 100644 --- a/src/EditorFeatures/TestUtilities/Remote/InProcRemostHostClient.cs +++ b/src/EditorFeatures/TestUtilities/Remote/InProcRemostHostClient.cs @@ -31,7 +31,7 @@ public static async Task CreateAsync(Workspace workspace, bool { var inprocServices = new InProcRemoteServices(runCacheCleanup); - var remoteHostStream = await inprocServices.RequestServiceAsync(WellKnownRemoteHostServices.RemoteHostService).ConfigureAwait(false); + var remoteHostStream = await inprocServices.RequestServiceAsync(WellKnownServiceHubServices.RemoteHostService).ConfigureAwait(false); var current = CreateClientId(Process.GetCurrentProcess().Id.ToString()); var instance = new InProcRemoteHostClient(current, workspace, inprocServices, remoteHostStream); @@ -98,7 +98,7 @@ public Task RequestServiceAsync(string serviceName) public override string ClientId { get; } public override bool IsRemoteHost64Bit => IntPtr.Size == 8; - public override async Task TryCreateConnectionAsync( + protected override async Task TryCreateConnectionAsync( string serviceName, object? callbackTarget, CancellationToken cancellationToken) { // get stream from service hub to communicate service specific information @@ -108,10 +108,6 @@ public Task RequestServiceAsync(string serviceName) return new JsonRpcConnection(Workspace, _inprocServices.Logger, callbackTarget, serviceStream); } - protected override void OnStarted() - { - } - public override void Dispose() { // we are asked to disconnect. unsubscribe and dispose to disconnect @@ -165,7 +161,7 @@ public InProcRemoteServices(bool runCacheCleanup) _serviceProvider = new ServiceProvider(runCacheCleanup); _creatorMap = new Dictionary>(); - RegisterService(WellKnownRemoteHostServices.RemoteHostService, (s, p) => new RemoteHostService(s, p)); + RegisterService(WellKnownServiceHubServices.RemoteHostService, (s, p) => new RemoteHostService(s, p)); RegisterService(WellKnownServiceHubServices.CodeAnalysisService, (s, p) => new CodeAnalysisService(s, p)); RegisterService(WellKnownServiceHubServices.RemoteSymbolSearchUpdateEngine, (s, p) => new RemoteSymbolSearchUpdateEngine(s, p)); RegisterService(WellKnownServiceHubServices.RemoteDesignerAttributeService, (s, p) => new RemoteDesignerAttributeService(s, p)); diff --git a/src/VisualStudio/Core/Def/Implementation/Remote/GlobalNotificationRemoteDeliveryService.cs b/src/VisualStudio/Core/Def/Implementation/Remote/GlobalNotificationRemoteDeliveryService.cs new file mode 100644 index 0000000000000..3fb2a341b9340 --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/Remote/GlobalNotificationRemoteDeliveryService.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Notification; +using Microsoft.CodeAnalysis.Remote; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Remote +{ + /// + /// Delivers global notifications to remote services. + /// + internal sealed class GlobalNotificationRemoteDeliveryService : IDisposable + { + private enum GlobalNotificationState + { + NotStarted, + Started + } + + /// + /// Lock for the task chain. Each time we hear + /// about a global operation starting or stopping (i.e. a build) we will '.ContinueWith' + /// this task chain with a new notification to the OOP side. This way all the messages + /// are properly serialized and appear in the right order (i.e. we don't hear about a + /// stop prior to hearing about the relevant start). + /// + private readonly object _globalNotificationsGate = new object(); + private Task _globalNotificationsTask = Task.FromResult(GlobalNotificationState.NotStarted); + + private readonly HostWorkspaceServices _services; + private readonly CancellationToken _cancellationToken; + + public GlobalNotificationRemoteDeliveryService(HostWorkspaceServices services, CancellationToken cancellationToken) + { + _services = services; + _cancellationToken = cancellationToken; + + RegisterGlobalOperationNotifications(); + } + + public void Dispose() + { + UnregisterGlobalOperationNotifications(); + } + + private void RegisterGlobalOperationNotifications() + { + var globalOperationService = _services.GetService(); + if (globalOperationService != null) + { + globalOperationService.Started += OnGlobalOperationStarted; + globalOperationService.Stopped += OnGlobalOperationStopped; + } + } + + private void UnregisterGlobalOperationNotifications() + { + var globalOperationService = _services.GetService(); + if (globalOperationService != null) + { + globalOperationService.Started -= OnGlobalOperationStarted; + globalOperationService.Stopped -= OnGlobalOperationStopped; + } + } + + private void OnGlobalOperationStarted(object sender, EventArgs e) + { + lock (_globalNotificationsGate) + { + _globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync( + SendStartNotificationAsync, _cancellationToken, TaskContinuationOptions.None, TaskScheduler.Default); + } + } + + private async Task SendStartNotificationAsync(Task previousTask) + { + // Can only transition from NotStarted->Started. If we hear about + // anything else, do nothing. + if (previousTask.Result != GlobalNotificationState.NotStarted) + { + return previousTask.Result; + } + + var client = await RemoteHostClient.TryGetClientAsync(_services, _cancellationToken).ConfigureAwait(false); + if (client == null) + { + return previousTask.Result; + } + + _ = await client.TryRunRemoteAsync( + WellKnownServiceHubServices.CodeAnalysisService, + nameof(IRemoteGlobalNotificationDeliveryService.OnGlobalOperationStarted), + solution: null, + Array.Empty(), + callbackTarget: null, + _cancellationToken).ConfigureAwait(false); + + return GlobalNotificationState.Started; + } + + private void OnGlobalOperationStopped(object sender, GlobalOperationEventArgs e) + { + lock (_globalNotificationsGate) + { + _globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync( + previous => SendStoppedNotificationAsync(previous, e), _cancellationToken, TaskContinuationOptions.None, TaskScheduler.Default); + } + } + + private async Task SendStoppedNotificationAsync(Task previousTask, GlobalOperationEventArgs e) + { + // Can only transition from Started->NotStarted. If we hear about + // anything else, do nothing. + if (previousTask.Result != GlobalNotificationState.Started) + { + return previousTask.Result; + } + + var client = await RemoteHostClient.TryGetClientAsync(_services, _cancellationToken).ConfigureAwait(false); + if (client == null) + { + return previousTask.Result; + } + + _ = await client.TryRunRemoteAsync( + WellKnownServiceHubServices.CodeAnalysisService, + nameof(IRemoteGlobalNotificationDeliveryService.OnGlobalOperationStopped), + solution: null, + new object[] { e.Operations, e.Cancelled }, + callbackTarget: null, + _cancellationToken).ConfigureAwait(false); + + // Mark that we're stopped now. + return GlobalNotificationState.NotStarted; + } + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/Remote/RemoteHostClientServiceFactory.RemoteHostClientService.cs b/src/VisualStudio/Core/Def/Implementation/Remote/RemoteHostClientServiceFactory.RemoteHostClientService.cs index 490de40adc2d7..4457a5f2bb023 100644 --- a/src/VisualStudio/Core/Def/Implementation/Remote/RemoteHostClientServiceFactory.RemoteHostClientService.cs +++ b/src/VisualStudio/Core/Def/Implementation/Remote/RemoteHostClientServiceFactory.RemoteHostClientService.cs @@ -8,15 +8,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Execution; -using Microsoft.CodeAnalysis.Experiments; using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; -using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Shared.TestHooks; using Roslyn.Utilities; @@ -34,6 +29,8 @@ public sealed class RemoteHostClientService : ForegroundThreadAffinitizedObject, private readonly IAsynchronousOperationListener _listener; private readonly Workspace _workspace; + private GlobalNotificationRemoteDeliveryService? _globalNotificationDelivery; + private readonly object _gate; private SolutionChecksumUpdater? _checksumUpdater; @@ -91,8 +88,8 @@ public void Enable() var token = _shutdownCancellationTokenSource.Token; - // create solution checksum updater - _checksumUpdater = new SolutionChecksumUpdater(this, token); + _checksumUpdater = new SolutionChecksumUpdater(Workspace, Listener, token); + _globalNotificationDelivery = new GlobalNotificationRemoteDeliveryService(Workspace.Services, token); _remoteClientTask = Task.Run(() => EnableAsync(token), token); } @@ -115,12 +112,15 @@ public void Disable() Contract.ThrowIfNull(_shutdownCancellationTokenSource); Contract.ThrowIfNull(_checksumUpdater); + Contract.ThrowIfNull(_globalNotificationDelivery); _shutdownCancellationTokenSource.Cancel(); _checksumUpdater.Shutdown(); _checksumUpdater = null; + _globalNotificationDelivery.Dispose(); + try { remoteClientTask.Wait(_shutdownCancellationTokenSource.Token); @@ -192,7 +192,6 @@ private void SetRemoteHostBitness() Logger.Log(FunctionId.RemoteHost_Bitness, KeyValueLogMessage.Create(LogType.Trace, m => m["64bit"] = x64)); // set service bitness - WellKnownRemoteHostServices.Set64bit(x64); WellKnownServiceHubServices.Set64bit(x64); } diff --git a/src/VisualStudio/Core/Def/Implementation/Remote/RemoteHostClientServiceFactory.SolutionChecksumUpdater.cs b/src/VisualStudio/Core/Def/Implementation/Remote/RemoteHostClientServiceFactory.SolutionChecksumUpdater.cs deleted file mode 100644 index 9ba3e25b456e1..0000000000000 --- a/src/VisualStudio/Core/Def/Implementation/Remote/RemoteHostClientServiceFactory.SolutionChecksumUpdater.cs +++ /dev/null @@ -1,222 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.Notification; -using Microsoft.CodeAnalysis.Remote; -using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.CodeAnalysis.SolutionCrawler; -using Roslyn.Utilities; - -namespace Microsoft.VisualStudio.LanguageServices.Remote -{ - internal partial class RemoteHostClientServiceFactory - { - private class SolutionChecksumUpdater : GlobalOperationAwareIdleProcessor - { - private readonly RemoteHostClientService _service; - private readonly TaskQueue _textChangeQueue; - private readonly SemaphoreSlim _event; - private readonly object _gate; - - private CancellationTokenSource _globalOperationCancellationSource; - - // hold last async token - private IAsyncToken _lastToken; - - public SolutionChecksumUpdater(RemoteHostClientService service, CancellationToken shutdownToken) - : base(service.Listener, - service.Workspace.Services.GetService(), - service.Workspace.Options.GetOption(RemoteHostOptions.SolutionChecksumMonitorBackOffTimeSpanInMS), shutdownToken) - { - _service = service; - _textChangeQueue = new TaskQueue(service.Listener, TaskScheduler.Default); - - _event = new SemaphoreSlim(initialCount: 0); - _gate = new object(); - - // start listening workspace change event - _service.Workspace.WorkspaceChanged += OnWorkspaceChanged; - - // create its own cancellation token source - _globalOperationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken); - - Start(); - } - - private CancellationToken ShutdownCancellationToken => CancellationToken; - - protected override async Task ExecuteAsync() - { - lock (_gate) - { - _lastToken?.Dispose(); - _lastToken = null; - } - - // wait for global operation to finish - await GlobalOperationTask.ConfigureAwait(false); - - // update primary solution in remote host - await SynchronizePrimaryWorkspaceAsync(_globalOperationCancellationSource.Token).ConfigureAwait(false); - } - - protected override void PauseOnGlobalOperation() - { - var previousCancellationSource = _globalOperationCancellationSource; - - // create new cancellation token source linked with given shutdown cancellation token - _globalOperationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(ShutdownCancellationToken); - - CancelAndDispose(previousCancellationSource); - } - - protected override Task WaitAsync(CancellationToken cancellationToken) - => _event.WaitAsync(cancellationToken); - - public override void Shutdown() - { - base.Shutdown(); - - // stop listening workspace change event - _service.Workspace.WorkspaceChanged -= OnWorkspaceChanged; - - CancelAndDispose(_globalOperationCancellationSource); - } - - private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) - { - if (e.Kind == WorkspaceChangeKind.DocumentChanged) - { - PushTextChanges(e.OldSolution.GetDocument(e.DocumentId), e.NewSolution.GetDocument(e.DocumentId)); - } - - // record that we are busy - UpdateLastAccessTime(); - - EnqueueChecksumUpdate(); - } - - private void EnqueueChecksumUpdate() - { - // event will raised sequencially. no concurrency on this handler - if (_event.CurrentCount > 0) - { - return; - } - - lock (_gate) - { - _lastToken ??= Listener.BeginAsyncOperation(nameof(SolutionChecksumUpdater)); - } - - _event.Release(); - } - - private async Task SynchronizePrimaryWorkspaceAsync(CancellationToken cancellationToken) - { - var workspace = _service.Workspace; - var solution = workspace.CurrentSolution; - if (solution.BranchId != solution.Workspace.PrimaryBranchId) - { - return; - } - - var client = await RemoteHostClient.TryGetClientAsync(workspace, cancellationToken).ConfigureAwait(false); - if (client == null) - { - return; - } - - using (Logger.LogBlock(FunctionId.SolutionChecksumUpdater_SynchronizePrimaryWorkspace, cancellationToken)) - { - var checksum = await solution.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false); - - _ = await client.TryRunRemoteAsync( - WellKnownRemoteHostServices.RemoteHostService, - nameof(IRemoteHostService.SynchronizePrimaryWorkspaceAsync), - solution, - new object[] { checksum, solution.WorkspaceVersion }, - callbackTarget: null, - cancellationToken).ConfigureAwait(false); - } - } - - - private static void CancelAndDispose(CancellationTokenSource cancellationSource) - { - // cancel running tasks - cancellationSource.Cancel(); - - // dispose cancellation token source - cancellationSource.Dispose(); - } - - private void PushTextChanges(Document oldDocument, Document newDocument) - { - // this pushes text changes to the remote side if it can. - // this is purely perf optimization. whether this pushing text change - // worked or not doesn't affect feature's functionality. - // - // this basically see whether it can cheaply find out text changes - // between 2 snapshots, if it can, it will send out that text changes to - // remote side. - // - // the remote side, once got the text change, will again see whether - // it can use that text change information without any high cost and - // create new snapshot from it. - // - // otherwise, it will do the normal behavior of getting full text from - // VS side. this optimization saves times we need to do full text - // synchronization for typing scenario. - - if ((oldDocument.TryGetText(out var oldText) == false) || - (newDocument.TryGetText(out var newText) == false)) - { - // we only support case where text already exist - return; - } - - // get text changes - var textChanges = newText.GetTextChanges(oldText); - if (textChanges.Count == 0) - { - // no changes - return; - } - - // whole document case - if (textChanges.Count == 1 && textChanges[0].Span.Length == oldText.Length) - { - // no benefit here. pulling from remote host is more efficient - return; - } - - // only cancelled when remote host gets shutdown - _textChangeQueue.ScheduleTask(nameof(PushTextChanges), async () => - { - var client = await RemoteHostClient.TryGetClientAsync(_service.Workspace, CancellationToken).ConfigureAwait(false); - if (client == null) - { - return; - } - - var state = await oldDocument.State.GetStateChecksumsAsync(CancellationToken).ConfigureAwait(false); - - _ = await client.TryRunRemoteAsync( - WellKnownRemoteHostServices.RemoteHostService, - nameof(IRemoteHostService.SynchronizeTextAsync), - solution: null, - new object[] { oldDocument.Id, state.Text, textChanges }, - callbackTarget: null, - CancellationToken).ConfigureAwait(false); - - }, CancellationToken); - } - } - } -} diff --git a/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.ConnectionManager.cs b/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.ConnectionManager.cs index 71e9f4d64b8f8..79a6cdc73bb66 100644 --- a/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.ConnectionManager.cs +++ b/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.ConnectionManager.cs @@ -8,112 +8,54 @@ using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Remote; -using Microsoft.ServiceHub.Client; using Roslyn.Utilities; namespace Microsoft.VisualStudio.LanguageServices.Remote { internal sealed partial class ServiceHubRemoteHostClient { - private partial class ConnectionManager : IDisposable - { - private readonly Workspace _workspace; - private readonly HubClient _hubClient; - private readonly HostGroup _hostGroup; + private delegate Task ConnectionFactory(string serviceName, CancellationToken cancellationToken); + private sealed partial class ConnectionPool : IDisposable + { + private readonly ConnectionFactory _connectionFactory; private readonly ReaderWriterLockSlim _shutdownLock; - private readonly int _maxPoolConnections; // keyed to serviceName. each connection is for specific service such as CodeAnalysisService - private readonly ConcurrentDictionary> _pools; - - // indicate whether pool should be used. - private readonly bool _enableConnectionPool; + private readonly ConcurrentDictionary> _pools; private bool _isDisposed; - public ConnectionManager( - Workspace workspace, - HubClient hubClient, - HostGroup hostGroup, - bool enableConnectionPool, - int maxPoolConnection) + public ConnectionPool(ConnectionFactory connectionFactory, int maxPoolConnection) { - _workspace = workspace; - _hubClient = hubClient; - _hostGroup = hostGroup; - + _connectionFactory = connectionFactory; _maxPoolConnections = maxPoolConnection; // initial value 4 is chosen to stop concurrent dictionary creating too many locks. // and big enough for all our services such as codeanalysis, remotehost, snapshot and etc services - _pools = new ConcurrentDictionary>(concurrencyLevel: 4, capacity: 4); + _pools = new ConcurrentDictionary>(concurrencyLevel: 4, capacity: 4); - _enableConnectionPool = enableConnectionPool; _shutdownLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); } - public HostGroup HostGroup => _hostGroup; - - public Task CreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken) - { - // pool is not enabled by option - if (!_enableConnectionPool) - { - // RemoteHost is allowed to be restarted by IRemoteHostClientService.RequestNewRemoteHostAsync - // when that happens, existing Connection will keep working until they get disposed. - // - // now question is when someone calls RemoteHostClient.TryGetConnection for the client that got - // shutdown, whether it should gracefully handle that request or fail after shutdown. - // for current expected usage case where new remoteHost is only created when new solution is added, - // we should be fine on failing after shutdown. - // - // but, at some point, if we want to support RemoteHost being restarted at any random point, - // we need to revisit this to support such case by creating new temporary connections. - // for now, I dropped it since it felt over-designing when there is no usage case for that yet. - return CreateNewConnectionAsync(serviceName, callbackTarget, cancellationToken); - } - - // when callbackTarget is given, we can't share/pool connection since callbackTarget attaches a state to connection. - // so connection is only valid for that specific callbackTarget. it is up to the caller to keep connection open - // if he wants to reuse same connection - if (callbackTarget != null) - { - return CreateNewConnectionAsync(serviceName, callbackTarget, cancellationToken); - } - - return GetConnectionFromPoolAsync(serviceName, cancellationToken); - } - - private async Task GetConnectionFromPoolAsync(string serviceName, CancellationToken cancellationToken) + public async Task GetOrCreateConnectionAsync(string serviceName, CancellationToken cancellationToken) { - var queue = _pools.GetOrAdd(serviceName, _ => new ConcurrentQueue()); + var queue = _pools.GetOrAdd(serviceName, _ => new ConcurrentQueue()); if (queue.TryDequeue(out var connection)) { return new PooledConnection(this, serviceName, connection); } - var newConnection = await CreateNewConnectionAsync(serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false); - return new PooledConnection(this, serviceName, (JsonRpcConnection)newConnection); + var newConnection = await _connectionFactory(serviceName, cancellationToken).ConfigureAwait(false); + return new PooledConnection(this, serviceName, newConnection); } - private async Task CreateNewConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken) - { - // get stream from service hub to communicate service specific information - // this is what consumer actually use to communicate information - var serviceStream = await RequestServiceAsync(_workspace, _hubClient, serviceName, _hostGroup, cancellationToken).ConfigureAwait(false); - - return new JsonRpcConnection(_workspace, _hubClient.Logger, callbackTarget, serviceStream); - } - - private void Free(string serviceName, JsonRpcConnection connection) + private void Free(string serviceName, Connection connection) { using (_shutdownLock.DisposableRead()) { - if (!_enableConnectionPool || _isDisposed) + if (_isDisposed) { // pool is not being used or // manager is already shutdown @@ -152,8 +94,6 @@ public void Dispose() _pools.Clear(); } - - _hubClient.Dispose(); } } } diff --git a/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.PooledConnection.cs b/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.PooledConnection.cs index 5a177d54b744e..17ae1b7b7a6d9 100644 --- a/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.PooledConnection.cs +++ b/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.PooledConnection.cs @@ -7,23 +7,22 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Remote; namespace Microsoft.VisualStudio.LanguageServices.Remote { internal sealed partial class ServiceHubRemoteHostClient { - private partial class ConnectionManager + private partial class ConnectionPool { private class PooledConnection : Connection { - private readonly ConnectionManager _connectionManager; + private readonly ConnectionPool _pool; private readonly string _serviceName; - private readonly JsonRpcConnection _connection; + private readonly Connection _connection; - public PooledConnection(ConnectionManager pools, string serviceName, JsonRpcConnection connection) + public PooledConnection(ConnectionPool pool, string serviceName, Connection connection) { - _connectionManager = pools; + _pool = pool; _serviceName = serviceName; _connection = connection; } @@ -39,7 +38,7 @@ public override Task InvokeAsync(string targetName, IReadOnlyList protected override void DisposeImpl() { - _connectionManager.Free(_serviceName, _connection); + _pool.Free(_serviceName, _connection); base.DisposeImpl(); } } diff --git a/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.cs b/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.cs index 259e191fcd060..9a9a4d90a8bc9 100644 --- a/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.cs +++ b/src/VisualStudio/Core/Def/Implementation/Remote/ServiceHubRemoteHostClient.cs @@ -27,39 +27,32 @@ namespace Microsoft.VisualStudio.LanguageServices.Remote { internal sealed partial class ServiceHubRemoteHostClient : RemoteHostClient, IRemoteHostServiceCallback { - private enum GlobalNotificationState - { - NotStarted, - Started, - Finished - } - private readonly RemoteEndPoint _endPoint; - private readonly ConnectionManager _connectionManager; - private readonly CancellationTokenSource _shutdownCancellationTokenSource; + private readonly HubClient _hubClient; + private readonly HostGroup _hostGroup; - /// - /// Lock for the task chain. Each time we hear - /// about a global operation starting or stopping (i.e. a build) we will '.ContinueWith' - /// this task chain with a new notification to the OOP side. This way all the messages - /// are properly serialized and appera in the right order (i.e. we don't hear about a - /// stop prior to hearing about the relevant start). - /// - private readonly object _globalNotificationsGate = new object(); - private Task _globalNotificationsTask = Task.FromResult(GlobalNotificationState.NotStarted); + private readonly ConnectionPool? _connectionPool; private ServiceHubRemoteHostClient( Workspace workspace, - TraceSource logger, - ConnectionManager connectionManager, + HubClient hubClient, + HostGroup hostGroup, Stream stream) : base(workspace) { - _shutdownCancellationTokenSource = new CancellationTokenSource(); + if (workspace.Options.GetOption(RemoteHostOptions.EnableConnectionPool)) + { + int maxPoolConnection = workspace.Options.GetOption(RemoteHostOptions.MaxPoolConnection); + + _connectionPool = new ConnectionPool( + connectionFactory: (serviceName, cancellationToken) => CreateConnectionAsync(serviceName, callbackTarget: null, cancellationToken), + maxPoolConnection); + } - _connectionManager = connectionManager; + _hubClient = hubClient; + _hostGroup = hostGroup; - _endPoint = new RemoteEndPoint(stream, logger, incomingCallTarget: this); + _endPoint = new RemoteEndPoint(stream, hubClient.Logger, incomingCallTarget: this); _endPoint.Disconnected += OnDisconnected; _endPoint.UnexpectedExceptionThrown += OnUnexpectedExceptionThrown; _endPoint.StartListening(); @@ -72,9 +65,6 @@ private void OnUnexpectedExceptionThrown(Exception unexpectedException) { using (Logger.LogBlock(FunctionId.ServiceHubRemoteHostClient_CreateAsync, cancellationToken)) { - var enableConnectionPool = workspace.Options.GetOption(RemoteHostOptions.EnableConnectionPool); - var maxConnection = workspace.Options.GetOption(RemoteHostOptions.MaxPoolConnection); - // let each client to have unique id so that we can distinguish different clients when service is restarted var clientId = CreateClientId(Process.GetCurrentProcess().Id.ToString()); @@ -84,10 +74,9 @@ private void OnUnexpectedExceptionThrown(Exception unexpectedException) // use the hub client logger for unexpected exceptions from devenv as well, so we have complete information in the log: WatsonReporter.InitializeLogger(hubClient.Logger); - var remoteHostStream = await RequestServiceAsync(workspace, hubClient, WellKnownRemoteHostServices.RemoteHostService, hostGroup, cancellationToken).ConfigureAwait(false); - var connectionManager = new ConnectionManager(workspace, hubClient, hostGroup, enableConnectionPool, maxConnection); + var remoteHostStream = await RequestServiceAsync(workspace, hubClient, WellKnownServiceHubServices.RemoteHostService, hostGroup, cancellationToken).ConfigureAwait(false); - var client = new ServiceHubRemoteHostClient(workspace, hubClient.Logger, connectionManager, remoteHostStream); + var client = new ServiceHubRemoteHostClient(workspace, hubClient, hostGroup, remoteHostStream); var uiCultureLCID = CultureInfo.CurrentUICulture.LCID; var cultureLCID = CultureInfo.CurrentCulture.LCID; @@ -157,136 +146,45 @@ static bool ReportNonFatalWatson(Exception e, CancellationToken cancellationToke } } - public override string ClientId => _connectionManager.HostGroup.Id; - public override bool IsRemoteHost64Bit => RemoteHostOptions.IsServiceHubProcess64Bit(Workspace); - - public override Task TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken) - => _connectionManager.CreateConnectionAsync(serviceName, callbackTarget, cancellationToken).AsNullable(); - - protected override void OnStarted() - => RegisterGlobalOperationNotifications(); - - public override void Dispose() - { - // cancel all pending async work - _shutdownCancellationTokenSource.Cancel(); - - // we are asked to stop. unsubscribe and dispose to disconnect. - // there are 2 ways to get disconnected. one is Roslyn decided to disconnect with RemoteHost (ex, cancellation or recycle OOP) and - // the other is external thing disconnecting remote host from us (ex, user killing OOP process). - // the Disconnected event we subscribe is to detect #2 case. and this method is for #1 case. so when we are willingly disconnecting - // we don't need the event, otherwise, Disconnected event will be called twice. - UnregisterGlobalOperationNotifications(); + public HostGroup HostGroup => _hostGroup; - _endPoint.Disconnected -= OnDisconnected; - _endPoint.UnexpectedExceptionThrown -= OnUnexpectedExceptionThrown; - _endPoint.Dispose(); - - _connectionManager.Dispose(); - - base.Dispose(); - } - - public HostGroup HostGroup - { - get - { - Debug.Assert(_connectionManager.HostGroup.Id == ClientId); - return _connectionManager.HostGroup; - } - } - - #region Global Operation Notifications - - private void RegisterGlobalOperationNotifications() - { - var globalOperationService = this.Workspace.Services.GetService(); - if (globalOperationService != null) - { - globalOperationService.Started += OnGlobalOperationStarted; - globalOperationService.Stopped += OnGlobalOperationStopped; - } - } + public override string ClientId => _hostGroup.Id; + public override bool IsRemoteHost64Bit => RemoteHostOptions.IsServiceHubProcess64Bit(Workspace); - private void UnregisterGlobalOperationNotifications() + protected override Task TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken) { - var globalOperationService = this.Workspace.Services.GetService(); - if (globalOperationService != null) - { - globalOperationService.Started -= OnGlobalOperationStarted; - globalOperationService.Stopped -= OnGlobalOperationStopped; - } + // When callbackTarget is given, we can't share/pool connection since callbackTarget attaches a state to connection. + // so connection is only valid for that specific callbackTarget. it is up to the caller to keep connection open + // if he wants to reuse same connection. - Task localTask; - lock (_globalNotificationsGate) + if (callbackTarget == null && _connectionPool != null) { - // Unilaterally transition us to the finished state. Once we're finished - // we cannot start or stop anymore. - _globalNotificationsTask = _globalNotificationsTask.ContinueWith( - _ => GlobalNotificationState.Finished, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); - localTask = _globalNotificationsTask; + return _connectionPool.GetOrCreateConnectionAsync(serviceName, cancellationToken).AsNullable(); } - // Have to wait for all the notifications to make it to the OOP side so we keep - // it in a consistent state. Also, if we don't do this, our _rpc object will - // get disposed while we're remoting over the messages to the oop side. - localTask.Wait(); + return CreateConnectionAsync(serviceName, callbackTarget, cancellationToken).AsNullable(); } - private void OnGlobalOperationStarted(object sender, EventArgs e) + private async Task CreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken) { - lock (_globalNotificationsGate) - { - _globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync( - continuation, _shutdownCancellationTokenSource.Token, TaskContinuationOptions.None, TaskScheduler.Default); - } - - async Task continuation(Task previousTask) - { - // Can only transition from NotStarted->Started. If we hear about - // anything else, do nothing. - if (previousTask.Result != GlobalNotificationState.NotStarted) - { - return previousTask.Result; - } - - await _endPoint.InvokeAsync( - nameof(IRemoteHostService.OnGlobalOperationStarted), - new object[] { "" }, - _shutdownCancellationTokenSource.Token).ConfigureAwait(false); - - return GlobalNotificationState.Started; - } + var serviceStream = await RequestServiceAsync(Workspace, _hubClient, serviceName, _hostGroup, cancellationToken).ConfigureAwait(false); + return new JsonRpcConnection(Workspace, _hubClient.Logger, callbackTarget, serviceStream); } - private void OnGlobalOperationStopped(object sender, GlobalOperationEventArgs e) + public override void Dispose() { - lock (_globalNotificationsGate) - { - _globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync( - continuation, _shutdownCancellationTokenSource.Token, TaskContinuationOptions.None, TaskScheduler.Default); - } - - async Task continuation(Task previousTask) - { - // Can only transition from Started->NotStarted. If we hear about - // anything else, do nothing. - if (previousTask.Result != GlobalNotificationState.Started) - { - return previousTask.Result; - } + _endPoint.Disconnected -= OnDisconnected; + _endPoint.UnexpectedExceptionThrown -= OnUnexpectedExceptionThrown; + _endPoint.Dispose(); - await _endPoint.InvokeAsync( - nameof(IRemoteHostService.OnGlobalOperationStopped), - new object[] { e.Operations, e.Cancelled }, - _shutdownCancellationTokenSource.Token).ConfigureAwait(false); + _connectionPool?.Dispose(); + _hubClient.Dispose(); - // Mark that we're stopped now. - return GlobalNotificationState.NotStarted; - } + base.Dispose(); } - #endregion + private void OnDisconnected(JsonRpcDisconnectedEventArgs e) + => Dispose(); #region Assets @@ -328,8 +226,5 @@ public Task IsExperimentEnabledAsync(string experimentName, CancellationTo } #endregion - - private void OnDisconnected(JsonRpcDisconnectedEventArgs e) - => Dispose(); } } diff --git a/src/VisualStudio/Core/Def/Implementation/Remote/SolutionChecksumUpdater.cs b/src/VisualStudio/Core/Def/Implementation/Remote/SolutionChecksumUpdater.cs new file mode 100644 index 0000000000000..257b384d9a445 --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/Remote/SolutionChecksumUpdater.cs @@ -0,0 +1,218 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.Notification; +using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.SolutionCrawler; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Remote +{ + internal sealed class SolutionChecksumUpdater : GlobalOperationAwareIdleProcessor + { + private readonly Workspace _workspace; + private readonly TaskQueue _textChangeQueue; + private readonly SemaphoreSlim _event; + private readonly object _gate; + + private CancellationTokenSource _globalOperationCancellationSource; + + // hold last async token + private IAsyncToken _lastToken; + + public SolutionChecksumUpdater(Workspace workspace, IAsynchronousOperationListener listener, CancellationToken shutdownToken) + : base(listener, + workspace.Services.GetService(), + workspace.Options.GetOption(RemoteHostOptions.SolutionChecksumMonitorBackOffTimeSpanInMS), shutdownToken) + { + _workspace = workspace; + _textChangeQueue = new TaskQueue(listener, TaskScheduler.Default); + + _event = new SemaphoreSlim(initialCount: 0); + _gate = new object(); + + // start listening workspace change event + _workspace.WorkspaceChanged += OnWorkspaceChanged; + + // create its own cancellation token source + _globalOperationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken); + + Start(); + } + + private CancellationToken ShutdownCancellationToken => CancellationToken; + + protected override async Task ExecuteAsync() + { + lock (_gate) + { + _lastToken?.Dispose(); + _lastToken = null; + } + + // wait for global operation to finish + await GlobalOperationTask.ConfigureAwait(false); + + // update primary solution in remote host + await SynchronizePrimaryWorkspaceAsync(_globalOperationCancellationSource.Token).ConfigureAwait(false); + } + + protected override void PauseOnGlobalOperation() + { + var previousCancellationSource = _globalOperationCancellationSource; + + // create new cancellation token source linked with given shutdown cancellation token + _globalOperationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(ShutdownCancellationToken); + + CancelAndDispose(previousCancellationSource); + } + + protected override Task WaitAsync(CancellationToken cancellationToken) + => _event.WaitAsync(cancellationToken); + + public override void Shutdown() + { + base.Shutdown(); + + // stop listening workspace change event + _workspace.WorkspaceChanged -= OnWorkspaceChanged; + + CancelAndDispose(_globalOperationCancellationSource); + } + + private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) + { + if (e.Kind == WorkspaceChangeKind.DocumentChanged) + { + PushTextChanges(e.OldSolution.GetDocument(e.DocumentId), e.NewSolution.GetDocument(e.DocumentId)); + } + + // record that we are busy + UpdateLastAccessTime(); + + EnqueueChecksumUpdate(); + } + + private void EnqueueChecksumUpdate() + { + // event will raised sequencially. no concurrency on this handler + if (_event.CurrentCount > 0) + { + return; + } + + lock (_gate) + { + _lastToken ??= Listener.BeginAsyncOperation(nameof(SolutionChecksumUpdater)); + } + + _event.Release(); + } + + private async Task SynchronizePrimaryWorkspaceAsync(CancellationToken cancellationToken) + { + var solution = _workspace.CurrentSolution; + if (solution.BranchId != _workspace.PrimaryBranchId) + { + return; + } + + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + { + return; + } + + using (Logger.LogBlock(FunctionId.SolutionChecksumUpdater_SynchronizePrimaryWorkspace, cancellationToken)) + { + var checksum = await solution.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false); + + _ = await client.TryRunRemoteAsync( + WellKnownServiceHubServices.RemoteHostService, + nameof(IRemoteHostService.SynchronizePrimaryWorkspaceAsync), + solution, + new object[] { checksum, solution.WorkspaceVersion }, + callbackTarget: null, + cancellationToken).ConfigureAwait(false); + } + } + + + private static void CancelAndDispose(CancellationTokenSource cancellationSource) + { + // cancel running tasks + cancellationSource.Cancel(); + + // dispose cancellation token source + cancellationSource.Dispose(); + } + + private void PushTextChanges(Document oldDocument, Document newDocument) + { + // this pushes text changes to the remote side if it can. + // this is purely perf optimization. whether this pushing text change + // worked or not doesn't affect feature's functionality. + // + // this basically see whether it can cheaply find out text changes + // between 2 snapshots, if it can, it will send out that text changes to + // remote side. + // + // the remote side, once got the text change, will again see whether + // it can use that text change information without any high cost and + // create new snapshot from it. + // + // otherwise, it will do the normal behavior of getting full text from + // VS side. this optimization saves times we need to do full text + // synchronization for typing scenario. + + if ((oldDocument.TryGetText(out var oldText) == false) || + (newDocument.TryGetText(out var newText) == false)) + { + // we only support case where text already exist + return; + } + + // get text changes + var textChanges = newText.GetTextChanges(oldText); + if (textChanges.Count == 0) + { + // no changes + return; + } + + // whole document case + if (textChanges.Count == 1 && textChanges[0].Span.Length == oldText.Length) + { + // no benefit here. pulling from remote host is more efficient + return; + } + + // only cancelled when remote host gets shutdown + _textChangeQueue.ScheduleTask(nameof(PushTextChanges), async () => + { + var client = await RemoteHostClient.TryGetClientAsync(_workspace, CancellationToken).ConfigureAwait(false); + if (client == null) + { + return; + } + + var state = await oldDocument.State.GetStateChecksumsAsync(CancellationToken).ConfigureAwait(false); + + _ = await client.TryRunRemoteAsync( + WellKnownServiceHubServices.RemoteHostService, + nameof(IRemoteHostService.SynchronizeTextAsync), + solution: null, + new object[] { oldDocument.Id, state.Text, textChanges }, + callbackTarget: null, + CancellationToken).ConfigureAwait(false); + + }, CancellationToken); + } + } +} diff --git a/src/VisualStudio/Core/Test.Next/Services/LanguageServiceTests.cs b/src/VisualStudio/Core/Test.Next/Services/LanguageServiceTests.cs index 9b093d84cfac4..8405e31e7c36c 100644 --- a/src/VisualStudio/Core/Test.Next/Services/LanguageServiceTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/LanguageServiceTests.cs @@ -118,7 +118,7 @@ private async Task> GetVsSearchResultsAsync(Solution private async Task UpdatePrimaryWorkspace(InProcRemoteHostClient client, Solution solution) { Assert.True(await client.TryRunRemoteAsync( - WellKnownRemoteHostServices.RemoteHostService, + WellKnownServiceHubServices.RemoteHostService, nameof(IRemoteHostService.SynchronizePrimaryWorkspaceAsync), solution, new object[] { await solution.State.GetChecksumAsync(CancellationToken.None), _solutionVersion++ }, diff --git a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs index cba844d541aa3..2f4409eb74f33 100644 --- a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs @@ -115,7 +115,7 @@ public async Task TestRemoteHostTextSynchronize() // sync _ = await client.TryRunRemoteAsync( - WellKnownRemoteHostServices.RemoteHostService, + WellKnownServiceHubServices.RemoteHostService, nameof(IRemoteHostService.SynchronizeTextAsync), solution: null, new object[] { oldDocument.Id, oldState.Text, newText.GetTextChanges(oldText) }, @@ -481,7 +481,7 @@ private static (Project, Document) GetProjectAndDocument(Solution solution, stri private async Task UpdatePrimaryWorkspace(InProcRemoteHostClient client, Solution solution) { Assert.True(await client.TryRunRemoteAsync( - WellKnownRemoteHostServices.RemoteHostService, + WellKnownServiceHubServices.RemoteHostService, nameof(IRemoteHostService.SynchronizePrimaryWorkspaceAsync), solution, new object[] { await solution.State.GetChecksumAsync(CancellationToken.None), _solutionVersion++ }, diff --git a/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs b/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs index 8b930e5b1181d..75a225bf833a1 100644 --- a/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs @@ -146,15 +146,15 @@ public async Task TestCancellationOnSessionWithSolution() { var solution = workspace.CurrentSolution; var solutionChecksum = await solution.State.GetChecksumAsync(CancellationToken.None); + var service = solution.Workspace.Services.GetService(); var source = new CancellationTokenSource(); - using var connection = new InvokeThrowsCancellationConnection(source); - var exception = await Assert.ThrowsAnyAsync(() => SessionWithSolution.CreateAsync(connection, solution, source.Token)); + using var session = new KeepAliveSession(new InvokeThrowsCancellationConnection(source), service); + var exception = await Assert.ThrowsAnyAsync(() => SessionWithSolution.CreateAsync(session, solution, source.Token)); Assert.Equal(exception.CancellationToken, source.Token); // make sure things that should have been cleaned up are cleaned up - var service = (RemotableDataServiceFactory.Service)solution.Workspace.Services.GetService(); - Assert.Null(await service.TestOnly_GetRemotableDataAsync(solutionChecksum, CancellationToken.None).ConfigureAwait(false)); + Assert.Null(await ((RemotableDataServiceFactory.Service)service).TestOnly_GetRemotableDataAsync(solutionChecksum, CancellationToken.None).ConfigureAwait(false)); } } diff --git a/src/VisualStudio/Razor/RazorLanguageServiceClient.cs b/src/VisualStudio/Razor/RazorLanguageServiceClient.cs index 752a73726479c..8b3fc584ed163 100644 --- a/src/VisualStudio/Razor/RazorLanguageServiceClient.cs +++ b/src/VisualStudio/Razor/RazorLanguageServiceClient.cs @@ -39,8 +39,8 @@ public Task> TryRunRemoteAsync(string targetName, Solution? solut return null; } - var connection = await _client.TryCreateConnectionAsync(_serviceName, callbackTarget, cancellationToken).ConfigureAwait(false); - if (connection == null) + var keepAliveSession = await _client.TryCreateKeepAliveSessionAsync(_serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false); + if (keepAliveSession == null) { return null; } @@ -49,13 +49,13 @@ public Task> TryRunRemoteAsync(string targetName, Solution? solut try { // transfer ownership of the connection to the session object: - session = await SessionWithSolution.CreateAsync(connection, solution, cancellationToken).ConfigureAwait(false); + session = await SessionWithSolution.CreateAsync(keepAliveSession, solution, cancellationToken).ConfigureAwait(false); } finally { if (session == null) { - connection.Dispose(); + keepAliveSession.Dispose(); } } @@ -74,12 +74,12 @@ internal Session(SessionWithSolution inner) public Task InvokeAsync(string targetName, IReadOnlyList arguments, CancellationToken cancellationToken) { - return _inner.Connection.InvokeAsync(targetName, arguments, cancellationToken); + return _inner.KeepAliveSession.RunRemoteAsync(targetName, solution: null, arguments, cancellationToken); } public Task InvokeAsync(string targetName, IReadOnlyList arguments, CancellationToken cancellationToken) { - return _inner.Connection.InvokeAsync(targetName, arguments, cancellationToken); + return _inner.KeepAliveSession.RunRemoteAsync(targetName, solution: null, arguments, cancellationToken); } public void Dispose() diff --git a/src/VisualStudio/VisualStudioDiagnosticsToolWindow/OptionPages/PerformanceLoggersPage.cs b/src/VisualStudio/VisualStudioDiagnosticsToolWindow/OptionPages/PerformanceLoggersPage.cs index 1e42de10ae20b..13dad016a4487 100644 --- a/src/VisualStudio/VisualStudioDiagnosticsToolWindow/OptionPages/PerformanceLoggersPage.cs +++ b/src/VisualStudio/VisualStudioDiagnosticsToolWindow/OptionPages/PerformanceLoggersPage.cs @@ -70,7 +70,7 @@ public static void SetLoggers(IGlobalOptionService optionService, IThreadingCont var functionIds = GetFunctionIds(options).ToList(); threadingContext.JoinableTaskFactory.Run(() => client.TryRunRemoteAsync( - WellKnownRemoteHostServices.RemoteHostService, + WellKnownServiceHubServices.RemoteHostService, nameof(IRemoteHostService.SetLoggingFunctionIds), solution: null, new object[] { loggerTypes, functionIds }, diff --git a/src/Workspaces/Core/Portable/ExternalAccess/Pythia/Api/PythiaRemoteHostClient.cs b/src/Workspaces/Core/Portable/ExternalAccess/Pythia/Api/PythiaRemoteHostClient.cs index 46c4127899e35..6c6d464a13e24 100644 --- a/src/Workspaces/Core/Portable/ExternalAccess/Pythia/Api/PythiaRemoteHostClient.cs +++ b/src/Workspaces/Core/Portable/ExternalAccess/Pythia/Api/PythiaRemoteHostClient.cs @@ -5,8 +5,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Execution; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; namespace Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api @@ -26,21 +24,7 @@ public static async Task> TryRunRemoteAsync(Workspace workspace, serviceName += "64"; } - using var connection = await client.TryCreateConnectionAsync(serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false); - if (connection == null) - { - return default; - } - - var remoteDataService = workspace.Services.GetRequiredService(); - - using var scope = await remoteDataService.CreatePinnedRemotableDataScopeAsync(solution, cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(arguments.Count + 1, out var argumentsBuilder); - - argumentsBuilder.Add(scope.SolutionInfo); - argumentsBuilder.AddRange(arguments); - - return await connection.InvokeAsync(targetName, argumentsBuilder, cancellationToken).ConfigureAwait(false); + return await client.TryRunRemoteAsync(serviceName, targetName, solution, arguments, callbackTarget: null, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Workspaces/Core/Portable/ExternalAccess/UnitTesting/Api/UnitTestingRemoteHostClientWrapper.cs b/src/Workspaces/Core/Portable/ExternalAccess/UnitTesting/Api/UnitTestingRemoteHostClientWrapper.cs index e6a202e2f03c0..c36133632eabf 100644 --- a/src/Workspaces/Core/Portable/ExternalAccess/UnitTesting/Api/UnitTestingRemoteHostClientWrapper.cs +++ b/src/Workspaces/Core/Portable/ExternalAccess/UnitTesting/Api/UnitTestingRemoteHostClientWrapper.cs @@ -27,8 +27,8 @@ public async Task TryCreateUnitTestingKeepAl public async Task TryCreateUnitingSessionWithSolutionWrapperAsync(string serviceName, Solution solution, CancellationToken cancellationToken) { - var connection = await UnderlyingObject.TryCreateConnectionAsync(serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false); - if (connection == null) + var keepAliveSession = await UnderlyingObject.TryCreateKeepAliveSessionAsync(serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false); + if (keepAliveSession == null) { return default; } @@ -37,13 +37,13 @@ public async Task TryCreateUnitingSession try { // transfer ownership of the connection to the session object: - session = await SessionWithSolution.CreateAsync(connection, solution, cancellationToken).ConfigureAwait(false); + session = await SessionWithSolution.CreateAsync(keepAliveSession, solution, cancellationToken).ConfigureAwait(false); } finally { if (session == null) { - connection.Dispose(); + keepAliveSession.Dispose(); } } diff --git a/src/Workspaces/Core/Portable/ExternalAccess/UnitTesting/Api/UnitTestingSessionWithSolutionWrapper.cs b/src/Workspaces/Core/Portable/ExternalAccess/UnitTesting/Api/UnitTestingSessionWithSolutionWrapper.cs index c1c6010ba2227..c8ab61c870f99 100644 --- a/src/Workspaces/Core/Portable/ExternalAccess/UnitTesting/Api/UnitTestingSessionWithSolutionWrapper.cs +++ b/src/Workspaces/Core/Portable/ExternalAccess/UnitTesting/Api/UnitTestingSessionWithSolutionWrapper.cs @@ -20,10 +20,10 @@ public UnitTestingSessionWithSolutionWrapper(SessionWithSolution underlyingObjec => UnderlyingObject = underlyingObject; public Task InvokeAsync(string targetName, IReadOnlyList arguments, CancellationToken cancellationToken) - => UnderlyingObject?.Connection.InvokeAsync(targetName, arguments, cancellationToken) ?? Task.CompletedTask; + => UnderlyingObject?.KeepAliveSession.RunRemoteAsync(targetName, solution: null, arguments, cancellationToken) ?? Task.CompletedTask; public Task InvokeAsync(string targetName, IReadOnlyList arguments, CancellationToken cancellationToken) - => UnderlyingObject?.Connection.InvokeAsync(targetName, arguments, cancellationToken); + => UnderlyingObject?.KeepAliveSession.RunRemoteAsync(targetName, solution: null, arguments, cancellationToken); public void Dispose() => UnderlyingObject?.Dispose(); diff --git a/src/Workspaces/Core/Portable/Remote/IRemoteGlobalNotificationDeliveryService.cs b/src/Workspaces/Core/Portable/Remote/IRemoteGlobalNotificationDeliveryService.cs new file mode 100644 index 0000000000000..33932ee12bb12 --- /dev/null +++ b/src/Workspaces/Core/Portable/Remote/IRemoteGlobalNotificationDeliveryService.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable + +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.Remote +{ + internal interface IRemoteGlobalNotificationDeliveryService + { + void OnGlobalOperationStarted(); + + void OnGlobalOperationStopped(IReadOnlyList operations, bool cancelled); + } +} diff --git a/src/Workspaces/Core/Portable/Remote/IRemoteHostService.cs b/src/Workspaces/Core/Portable/Remote/IRemoteHostService.cs index 25c23094d03e7..943bf9053aa13 100644 --- a/src/Workspaces/Core/Portable/Remote/IRemoteHostService.cs +++ b/src/Workspaces/Core/Portable/Remote/IRemoteHostService.cs @@ -20,13 +20,6 @@ internal interface IRemoteHostService /// void SetLoggingFunctionIds(List loggerTypes, List functionIds, CancellationToken cancellationToken); - /// - /// JsonRPC seems to have a problem with empty parameter lists. So passing a dummy parameter - /// just to make it work properly. - /// - void OnGlobalOperationStarted(string unused); - void OnGlobalOperationStopped(IReadOnlyList operations, bool cancelled); - /// /// Synchronize data to OOP proactively without anyone asking for it to make most of operation /// faster diff --git a/src/Workspaces/Core/Portable/Remote/RemoteHostClient.cs b/src/Workspaces/Core/Portable/Remote/RemoteHostClient.cs index dd28ce1141cae..ed440ead6de24 100644 --- a/src/Workspaces/Core/Portable/Remote/RemoteHostClient.cs +++ b/src/Workspaces/Core/Portable/Remote/RemoteHostClient.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Execution; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; +using Microsoft.CodeAnalysis.Host; #if DEBUG using System.Diagnostics; @@ -24,7 +25,7 @@ namespace Microsoft.CodeAnalysis.Remote /// /// user can create a connection to communicate with the server (remote host) through this client /// - internal abstract partial class RemoteHostClient : IDisposable + internal abstract class RemoteHostClient : IDisposable { public readonly Workspace Workspace; public event EventHandler? StatusChanged; @@ -52,16 +53,12 @@ protected RemoteHostClient(Workspace workspace) /// Creating session could fail if remote host is not available. one of example will be user killing /// remote host. /// - public abstract Task TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken); + protected abstract Task TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken); public abstract bool IsRemoteHost64Bit { get; } - protected abstract void OnStarted(); - protected void Started() { - OnStarted(); - OnStatusChanged(started: true); } @@ -85,8 +82,11 @@ public static string CreateClientId(string prefix) } public static Task TryGetClientAsync(Workspace workspace, CancellationToken cancellationToken) + => TryGetClientAsync(workspace.Services, cancellationToken); + + public static Task TryGetClientAsync(HostWorkspaceServices services, CancellationToken cancellationToken) { - var service = workspace.Services.GetService(); + var service = services.GetService(); if (service == null) { return SpecializedTasks.Null(); @@ -196,13 +196,8 @@ public NoOpClient(Workspace workspace) public override string ClientId => nameof(NoOpClient); public override bool IsRemoteHost64Bit => false; - public override Task TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken) + protected override Task TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken) => SpecializedTasks.Null(); - - protected override void OnStarted() - { - // do nothing - } } /// diff --git a/src/Workspaces/Core/Portable/Remote/RemoteHostSessionHelpers.cs b/src/Workspaces/Core/Portable/Remote/SessionWithSolution.cs similarity index 76% rename from src/Workspaces/Core/Portable/Remote/RemoteHostSessionHelpers.cs rename to src/Workspaces/Core/Portable/Remote/SessionWithSolution.cs index 6ed4e846a029f..ac09ff526bf7d 100644 --- a/src/Workspaces/Core/Portable/Remote/RemoteHostSessionHelpers.cs +++ b/src/Workspaces/Core/Portable/Remote/SessionWithSolution.cs @@ -15,12 +15,13 @@ namespace Microsoft.CodeAnalysis.Remote [Obsolete("Only used by Razor and LUT", error: false)] internal sealed class SessionWithSolution : IDisposable { - public readonly RemoteHostClient.Connection Connection; + internal readonly KeepAliveSession KeepAliveSession; private readonly PinnedRemotableDataScope _scope; - public static async Task CreateAsync(RemoteHostClient.Connection connection, Solution solution, CancellationToken cancellationToken) + + public static async Task CreateAsync(KeepAliveSession keepAliveSession, Solution solution, CancellationToken cancellationToken) { - Contract.ThrowIfNull(connection); + Contract.ThrowIfNull(keepAliveSession); Contract.ThrowIfNull(solution); var service = solution.Workspace.Services.GetRequiredService(); @@ -31,13 +32,14 @@ public static async Task CreateAsync(RemoteHostClient.Conne { // set connection state for this session. // we might remove this in future. see https://github.com/dotnet/roslyn/issues/24836 - await connection.InvokeAsync( + await keepAliveSession.RunRemoteAsync( WellKnownServiceHubServices.ServiceHubServiceBase_Initialize, + solution: null, new object[] { scope.SolutionInfo }, cancellationToken).ConfigureAwait(false); // transfer ownership of connection and scope to the session object: - session = new SessionWithSolution(connection, scope); + session = new SessionWithSolution(keepAliveSession, scope); } finally { @@ -50,16 +52,16 @@ await connection.InvokeAsync( return session; } - private SessionWithSolution(RemoteHostClient.Connection connection, PinnedRemotableDataScope scope) + private SessionWithSolution(KeepAliveSession keepAliveSession, PinnedRemotableDataScope scope) { - Connection = connection; + KeepAliveSession = keepAliveSession; _scope = scope; } public void Dispose() { _scope.Dispose(); - Connection.Dispose(); + KeepAliveSession.Dispose(); } } } diff --git a/src/Workspaces/Core/Portable/Remote/WellKnownRemoteHostServices.cs b/src/Workspaces/Core/Portable/Remote/WellKnownRemoteHostServices.cs deleted file mode 100644 index c0305079fdb11..0000000000000 --- a/src/Workspaces/Core/Portable/Remote/WellKnownRemoteHostServices.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Microsoft.CodeAnalysis.Remote -{ - internal static class WellKnownRemoteHostServices - { - public static void Set64bit(bool x64) - => RemoteHostService = "roslynRemoteHost" + (x64 ? "64" : ""); - - public static string RemoteHostService { get; private set; } = "roslynRemoteHost"; - } -} diff --git a/src/Workspaces/Core/Portable/Remote/WellKnownServiceHubServices.cs b/src/Workspaces/Core/Portable/Remote/WellKnownServiceHubServices.cs index 35127611bd20d..3bccda98921da 100644 --- a/src/Workspaces/Core/Portable/Remote/WellKnownServiceHubServices.cs +++ b/src/Workspaces/Core/Portable/Remote/WellKnownServiceHubServices.cs @@ -12,6 +12,7 @@ public static void Set64bit(bool x64) { var bit = x64 ? "64" : ""; + RemoteHostService = "roslynRemoteHost" + bit; CodeAnalysisService = NamePrefix + "CodeAnalysis" + bit; RemoteDesignerAttributeService = NamePrefix + "RemoteDesignerAttributeService" + bit; RemoteProjectTelemetryService = NamePrefix + "RemoteProjectTelemetryService" + bit; @@ -20,6 +21,7 @@ public static void Set64bit(bool x64) LanguageServer = NamePrefix + "LanguageServer" + bit; } + public static string RemoteHostService { get; private set; } = NamePrefix + "RemoteHost"; public static string CodeAnalysisService { get; private set; } = NamePrefix + "CodeAnalysis"; public static string RemoteSymbolSearchUpdateEngine { get; private set; } = NamePrefix + "RemoteSymbolSearchUpdateEngine"; public static string RemoteDesignerAttributeService { get; private set; } = NamePrefix + "RemoteDesignerAttributeService"; diff --git a/src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_GlobalNotifications.cs b/src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_GlobalNotifications.cs new file mode 100644 index 0000000000000..9d087b9aef517 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_GlobalNotifications.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable + +using System.Collections.Generic; +using System.Threading; +using Microsoft.CodeAnalysis.Notification; +using Microsoft.CodeAnalysis.Remote.Services; + +namespace Microsoft.CodeAnalysis.Remote +{ + internal partial class CodeAnalysisService : ServiceBase, IRemoteGlobalNotificationDeliveryService + { + /// + /// Remote API. + /// + public void OnGlobalOperationStarted() + { + RunService(() => + { + var globalOperationNotificationService = GetGlobalOperationNotificationService(); + globalOperationNotificationService?.OnStarted(); + }, CancellationToken.None); + } + + /// + /// Remote API. + /// + public void OnGlobalOperationStopped(IReadOnlyList operations, bool cancelled) + { + RunService(() => + { + var globalOperationNotificationService = GetGlobalOperationNotificationService(); + globalOperationNotificationService?.OnStopped(operations, cancelled); + }, CancellationToken.None); + } + + private RemoteGlobalOperationNotificationService? GetGlobalOperationNotificationService() + => SolutionService.PrimaryWorkspace.Services.GetService() as RemoteGlobalOperationNotificationService; + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Services/RemoteHostService.cs b/src/Workspaces/Remote/ServiceHub/Services/RemoteHostService.cs index 318d28e948b0b..ffb1f11520a54 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/RemoteHostService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/RemoteHostService.cs @@ -20,7 +20,6 @@ using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Notification; using Microsoft.CodeAnalysis.Remote.Diagnostics; -using Microsoft.CodeAnalysis.Remote.Services; using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServices.Telemetry; @@ -140,30 +139,6 @@ Task IAssetSource.IsExperimentEnabledAsync(string experimentName, Cancella }, cancellationToken); } - /// - /// Remote API. - /// - public void OnGlobalOperationStarted(string unused) - { - RunService(() => - { - var globalOperationNotificationService = GetGlobalOperationNotificationService(); - globalOperationNotificationService?.OnStarted(); - }, CancellationToken.None); - } - - /// - /// Remote API. - /// - public void OnGlobalOperationStopped(IReadOnlyList operations, bool cancelled) - { - RunService(() => - { - var globalOperationNotificationService = GetGlobalOperationNotificationService(); - globalOperationNotificationService?.OnStopped(operations, cancelled); - }, CancellationToken.None); - } - /// /// Remote API. /// @@ -270,12 +245,6 @@ private static bool ExpectedCultureIssue(Exception ex) return ex is ArgumentOutOfRangeException || ex is CultureNotFoundException; } - private RemoteGlobalOperationNotificationService? GetGlobalOperationNotificationService() - { - var notificationService = SolutionService.PrimaryWorkspace.Services.GetService() as RemoteGlobalOperationNotificationService; - return notificationService; - } - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr AddDllDirectory(string directory);