diff --git a/src/LanguageServer/roslyn-language-server/LspRelay.cs b/src/LanguageServer/roslyn-language-server/LspRelay.cs index 0d40341f92d64..66aa80e64610f 100644 --- a/src/LanguageServer/roslyn-language-server/LspRelay.cs +++ b/src/LanguageServer/roslyn-language-server/LspRelay.cs @@ -10,17 +10,11 @@ internal enum RelayEndpoint Server, } -internal readonly struct RelayResult(RelayEndpoint closedEndpoint, bool bothSidesClosed) +internal enum RelayCompletionKind { - /// The endpoint whose stream closed first, ending the relay. - public RelayEndpoint ClosedEndpoint { get; } = closedEndpoint; - - /// - /// True when, shortly after the first side closed, the other side also closed on its own. A clean LSP - /// shutdown closes both sides (the editor sends exit and closes; the server processes it and - /// closes), whereas a crash leaves one side connected. - /// - public bool BothSidesClosed { get; } = bothSidesClosed; + CleanShutdown, + EditorConnectionLost, + ServerConnectionLost, } internal static class LspRelay @@ -31,7 +25,7 @@ internal static class LspRelay /// private static readonly TimeSpan s_secondCloseGracePeriod = TimeSpan.FromSeconds(5); - public static async Task RelayAsync( + public static async Task RelayAsync( Stream fromEditor, Stream toEditor, Stream fromServer, @@ -42,14 +36,28 @@ public static async Task RelayAsync( var serverToEditor = CopyUntilClosedAsync(fromServer, toEditor, RelayEndpoint.Server, RelayEndpoint.Editor, cancellationSource.Token); var completedTask = await Task.WhenAny(editorToServer, serverToEditor).ConfigureAwait(false); - // Give the other direction a brief window to finish on its own. If it does, both sides closed, which - // indicates a clean shutdown rather than a crash on one side. + // Give the other direction a brief window to finish on its own. If both copies terminate at the server, + // the server connection was lost and caused both directions to stop. Any other pair is a clean shutdown: + // an editor closes its bidirectional transport after sending LSP 'exit', so both copies can terminate at + // the editor before the server closes its side. var otherTask = completedTask == editorToServer ? serverToEditor : editorToServer; - var bothSidesClosed = await Task.WhenAny(otherTask, Task.Delay(s_secondCloseGracePeriod)).ConfigureAwait(false) == otherTask; + RelayEndpoint? otherClosedEndpoint = null; + if (await Task.WhenAny(otherTask, Task.Delay(s_secondCloseGracePeriod)).ConfigureAwait(false) == otherTask) + otherClosedEndpoint = await otherTask.ConfigureAwait(false); cancellationSource.Cancel(); - var result = await completedTask.ConfigureAwait(false); - return new RelayResult(result, bothSidesClosed); + var closedEndpoint = await completedTask.ConfigureAwait(false); + + if (otherClosedEndpoint is not null) + { + return closedEndpoint == RelayEndpoint.Server && otherClosedEndpoint == RelayEndpoint.Server + ? RelayCompletionKind.ServerConnectionLost + : RelayCompletionKind.CleanShutdown; + } + + return closedEndpoint == RelayEndpoint.Editor + ? RelayCompletionKind.EditorConnectionLost + : RelayCompletionKind.ServerConnectionLost; } private static async Task CopyUntilClosedAsync( diff --git a/src/LanguageServer/roslyn-language-server/Program.cs b/src/LanguageServer/roslyn-language-server/Program.cs index 6de75e505f2b7..f371673c5e09f 100644 --- a/src/LanguageServer/roslyn-language-server/Program.cs +++ b/src/LanguageServer/roslyn-language-server/Program.cs @@ -71,27 +71,29 @@ private static async Task RelayDaemonAsync( Stream daemonStream, EditorConnection editorConnection) { - var relayResult = await LspRelay.RelayAsync( + var relayCompletionKind = await LspRelay.RelayAsync( editorConnection.Input, editorConnection.Output, daemonStream, daemonStream); - // A clean LSP shutdown closes both sides; report success so the editor doesn't treat it as a crash. - if (relayResult.BothSidesClosed) + switch (relayCompletionKind) { - Console.Error.WriteLine("Language server session ended cleanly."); - return ExitCodes.Success; - } + case RelayCompletionKind.CleanShutdown: + Console.Error.WriteLine("Language server session ended cleanly."); + return ExitCodes.Success; - if (relayResult.ClosedEndpoint == RelayEndpoint.Editor) - { - Console.Error.WriteLine("Editor connection closed before the language server daemon connection."); - return ExitCodes.EditorConnectionLost; - } + case RelayCompletionKind.EditorConnectionLost: + Console.Error.WriteLine("Editor connection closed before the language server daemon connection."); + return ExitCodes.EditorConnectionLost; - Console.Error.WriteLine("Language server daemon connection closed before the editor connection."); - return ExitCodes.ServerConnectionLost; + case RelayCompletionKind.ServerConnectionLost: + Console.Error.WriteLine("Language server daemon connection closed before the editor connection."); + return ExitCodes.ServerConnectionLost; + + default: + throw new InvalidOperationException($"Unexpected relay completion kind: {relayCompletionKind}"); + } } private static Task StartClientProcessMonitorAsync(int? processId)