Skip to content

Commit 5281160

Browse files
Treat missing .ConfigureAwait(false) as errors (and fix) (#633)
I enabled the new .NET Analyzer for the project and then used an Editor Config rule to treat CA2007 as an error. This diagnostic is for any awaited task that does not have an explicit trailing `.ConfigureAwait(false)` call, an annoying but very necessary configuration for asynchronous .NET library code (such as OmniSharp). The bugs caused by these missing calls are strange. In the case of PowerShell Editor Services, an LSP server using OmniSharp and powering the PowerShell Extension for VS Code, it showed up as a hang when the server executed user code that used objects from `System.Windows.Forms`. This is because that .NET library sets its own synchronization context, which is exactly where `ConfigureAwait(continueOnCapturedContext: false)` comes into play. The default value is `true` which roughly means that the awaited tasks will only run on their own context. So when the context is changed (because of `System.Windows.Forms` or other code introducing a new synchronization context) the task will never be continued, resulting in this hang. By configuring this to `false` we allow the tasks to continue regardless of the new context. See this .NET blog post for more details: https://devblogs.microsoft.com/dotnet/configureawait-faq/ Note that elsewhere in the codebase we've been very careful to set it to false, but we've not been perfect. Treating this diagnostic as an error will allow us to be as perfect as possible about it.
1 parent 1969872 commit 5281160

11 files changed

+32
-25
lines changed

.editorconfig

+4
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ resharper_web_config_module_not_resolved_highlighting=warning
128128
resharper_web_config_type_not_resolved_highlighting=warning
129129
resharper_web_config_wrong_module_highlighting=warning
130130

131+
# .NET Analzyer settings
132+
# VSTHRD200: Use "Async" suffix for awaitable methods
131133
dotnet_diagnostic.VSTHRD200.severity = none
134+
# CA2007: Do not directly await a Task
135+
dotnet_diagnostic.CA2007.severity = error
132136

133137
[*.{cs,cshtml}]
134138
charset=utf-8

Directory.Build.props

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<PackageProjectUrl>https://github.com/OmniSharp/csharp-language-server-protocol</PackageProjectUrl>
1414
<PackageTags>lsp;language server;language server protocol;language client;language server client</PackageTags>
1515
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\lsp.snk</AssemblyOriginatorKeyFile>
16+
<!-- See: https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview -->
17+
<EnableNETAnalyzers>true</EnableNETAnalyzers>
1618
</PropertyGroup>
1719
<PropertyGroup>
1820
<EmbedUntrackedSources>true</EmbedUntrackedSources>

sample/SampleServer/Program.cs

+8-8
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ private static async Task MainAsync(string[] args)
8383
);
8484
workDone = manager;
8585

86-
await Task.Delay(2000);
86+
await Task.Delay(2000).ConfigureAwait(false);
8787

8888
manager.OnNext(
8989
new WorkDoneProgressReport {
@@ -102,7 +102,7 @@ private static async Task MainAsync(string[] args)
102102
}
103103
);
104104

105-
await Task.Delay(2000);
105+
await Task.Delay(2000).ConfigureAwait(false);
106106

107107
workDone.OnNext(
108108
new WorkDoneProgressReport {
@@ -115,12 +115,12 @@ private static async Task MainAsync(string[] args)
115115
)
116116
.OnStarted(
117117
async (languageServer, token) => {
118-
using var manager = await languageServer.WorkDoneManager.Create(new WorkDoneProgressBegin { Title = "Doing some work..." });
118+
using var manager = await languageServer.WorkDoneManager.Create(new WorkDoneProgressBegin { Title = "Doing some work..." }).ConfigureAwait(false);
119119

120120
manager.OnNext(new WorkDoneProgressReport { Message = "doing things..." });
121-
await Task.Delay(10000);
121+
await Task.Delay(10000).ConfigureAwait(false);
122122
manager.OnNext(new WorkDoneProgressReport { Message = "doing things... 1234" });
123-
await Task.Delay(10000);
123+
await Task.Delay(10000).ConfigureAwait(false);
124124
manager.OnNext(new WorkDoneProgressReport { Message = "doing things... 56789" });
125125

126126
var logger = languageServer.Services.GetService<ILogger<Foo>>();
@@ -130,7 +130,7 @@ private static async Task MainAsync(string[] args)
130130
}, new ConfigurationItem {
131131
Section = "terminal",
132132
}
133-
);
133+
).ConfigureAwait(false);
134134

135135
var baseConfig = new JObject();
136136
foreach (var config in languageServer.Configuration.AsEnumerable())
@@ -149,9 +149,9 @@ private static async Task MainAsync(string[] args)
149149
logger.LogInformation("Scoped Config: {Config}", scopedConfig);
150150
}
151151
)
152-
);
152+
).ConfigureAwait(false);
153153

154-
await server.WaitForExit;
154+
await server.WaitForExit.ConfigureAwait(false);
155155
}
156156
}
157157

sample/SampleServer/SemanticTokensHandler.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ public SemanticTokensHandler(ILogger<SemanticTokensHandler> logger) =>
2525
SemanticTokensParams request, CancellationToken cancellationToken
2626
)
2727
{
28-
var result = await base.Handle(request, cancellationToken);
28+
var result = await base.Handle(request, cancellationToken).ConfigureAwait(false);
2929
return result;
3030
}
3131

3232
public override async Task<SemanticTokens?> Handle(
3333
SemanticTokensRangeParams request, CancellationToken cancellationToken
3434
)
3535
{
36-
var result = await base.Handle(request, cancellationToken);
36+
var result = await base.Handle(request, cancellationToken).ConfigureAwait(false);
3737
return result;
3838
}
3939

@@ -42,7 +42,7 @@ public SemanticTokensHandler(ILogger<SemanticTokensHandler> logger) =>
4242
CancellationToken cancellationToken
4343
)
4444
{
45-
var result = await base.Handle(request, cancellationToken);
45+
var result = await base.Handle(request, cancellationToken).ConfigureAwait(false);
4646
return result;
4747
}
4848

@@ -54,7 +54,7 @@ CancellationToken cancellationToken
5454
using var typesEnumerator = RotateEnum(SemanticTokenType.Defaults).GetEnumerator();
5555
using var modifiersEnumerator = RotateEnum(SemanticTokenModifier.Defaults).GetEnumerator();
5656
// you would normally get this from a common source that is managed by current open editor, current active editor, etc.
57-
var content = await File.ReadAllTextAsync(DocumentUri.GetFileSystemPath(identifier), cancellationToken);
57+
var content = await File.ReadAllTextAsync(DocumentUri.GetFileSystemPath(identifier), cancellationToken).ConfigureAwait(false);
5858
await Task.Yield();
5959

6060
foreach (var (line, text) in content.Split('\n').Select((text, line) => (line, text)))

sample/SampleServer/TextDocumentHandler.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public override async Task<Unit> Handle(DidOpenTextDocumentParams notification,
5151
{
5252
await Task.Yield();
5353
_logger.LogInformation("Hello world!");
54-
await _configuration.GetScopedConfiguration(notification.TextDocument.Uri, token);
54+
await _configuration.GetScopedConfiguration(notification.TextDocument.Uri, token).ConfigureAwait(false);
5555
return Unit.Value;
5656
}
5757

@@ -84,7 +84,7 @@ CancellationToken cancellationToken
8484
)
8585
{
8686
// you would normally get this from a common source that is managed by current open editor, current active editor, etc.
87-
var content = await File.ReadAllTextAsync(DocumentUri.GetFileSystemPath(request), cancellationToken);
87+
var content = await File.ReadAllTextAsync(DocumentUri.GetFileSystemPath(request), cancellationToken).ConfigureAwait(false);
8888
var lines = content.Split('\n');
8989
var symbols = new List<SymbolInformationOrDocumentSymbol>();
9090
for (var lineIndex = 0; lineIndex < lines.Length; lineIndex++)
@@ -160,31 +160,31 @@ CancellationToken cancellationToken
160160
using var partialResults = _progressManager.For(request, cancellationToken);
161161
if (partialResults != null)
162162
{
163-
await Task.Delay(2000, cancellationToken);
163+
await Task.Delay(2000, cancellationToken).ConfigureAwait(false);
164164

165165
reporter.OnNext(
166166
new WorkDoneProgressReport {
167167
Cancellable = true,
168168
Percentage = 20
169169
}
170170
);
171-
await Task.Delay(500, cancellationToken);
171+
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
172172

173173
reporter.OnNext(
174174
new WorkDoneProgressReport {
175175
Cancellable = true,
176176
Percentage = 40
177177
}
178178
);
179-
await Task.Delay(500, cancellationToken);
179+
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
180180

181181
reporter.OnNext(
182182
new WorkDoneProgressReport {
183183
Cancellable = true,
184184
Percentage = 50
185185
}
186186
);
187-
await Task.Delay(500, cancellationToken);
187+
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
188188

189189
partialResults.OnNext(
190190
new[] {
@@ -209,7 +209,7 @@ CancellationToken cancellationToken
209209
Percentage = 70
210210
}
211211
);
212-
await Task.Delay(500, cancellationToken);
212+
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
213213

214214
reporter.OnNext(
215215
new WorkDoneProgressReport {

src/Dap.Client/DebugAdapterClient.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ await DebugAdapterEventingHelper.Run(
154154
token
155155
).ConfigureAwait(false);
156156

157-
await _initializedComplete.ToTask(token, _scheduler);
157+
await _initializedComplete.ToTask(token, _scheduler).ConfigureAwait(false);
158158

159159
await DebugAdapterEventingHelper.Run(
160160
_startedDelegates,

src/Dap.Testing/DebugAdapterProtocolTestBase.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Action<DebugAdapterServerOptions> serverOptionsAction
7979

8080
return await Observable.FromAsync(_client.Initialize)
8181
.ForkJoin(Observable.FromAsync(_server.Initialize), (_, _) => ( _client, _server ))
82-
.ToTask(CancellationToken);
82+
.ToTask(CancellationToken).ConfigureAwait(false);
8383
}
8484
}
8585
}

src/JsonRpc/OutputHandler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ private async Task ProcessOutputStream(CancellationToken cancellationToken)
132132
{
133133
do
134134
{
135-
var value = await _queue.ReadAsync(cancellationToken);
135+
var value = await _queue.ReadAsync(cancellationToken).ConfigureAwait(false);
136136
if (value is ITraceData traceData)
137137
{
138138
_activityTracingStrategy?.ApplyOutgoing(traceData);

src/Protocol/Features/Document/CallHierarchyFeature.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ protected CallHierarchyHandlerBase() : this(Guid.NewGuid())
393393

394394
public sealed override async Task<Container<CallHierarchyItem>?> Handle(CallHierarchyPrepareParams request, CancellationToken cancellationToken)
395395
{
396-
var response = await HandlePrepare(request, cancellationToken);
396+
var response = await HandlePrepare(request, cancellationToken).ConfigureAwait(false);
397397
return Container<CallHierarchyItem>.From(response?.Select(CallHierarchyItem.From)!);
398398
}
399399

src/Server/LanguageServer.Shutdown.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ public partial class LanguageServer : IExitHandler, IShutdownHandler
2424
#pragma warning disable VSTHRD100
2525
public async void ForcefulShutdown()
2626
{
27-
await ( (IShutdownHandler) this ).Handle(ShutdownParams.Instance, CancellationToken.None);
28-
await ( (IExitHandler) this ).Handle(ExitParams.Instance, CancellationToken.None);
27+
await ( (IShutdownHandler) this ).Handle(ShutdownParams.Instance, CancellationToken.None).ConfigureAwait(false);
28+
await ( (IExitHandler) this ).Handle(ExitParams.Instance, CancellationToken.None).ConfigureAwait(false);
2929
}
3030

3131
async Task<Unit> IRequestHandler<ExitParams, Unit>.Handle(ExitParams request, CancellationToken token)

test/.editorconfig

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[*]
2+
dotnet_diagnostic.CA2007.severity = none
23
dotnet_diagnostic.CS0618.severity = none
34
dotnet_diagnostic.CS4014.severity = none
45
dotnet_diagnostic.CS1998.severity = none

0 commit comments

Comments
 (0)