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)