-
Notifications
You must be signed in to change notification settings - Fork 256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(lsp): kill language server if parent process dies #1664
fix(lsp): kill language server if parent process dies #1664
Conversation
…e language server
} | ||
catch { | ||
// If the process dies before we get here then request shutdown immediately | ||
cancelLanguageServer.Cancel(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will make it harder to test the server independently, because it will prevent us from replaying previously-recorded LSP traces. Can we find a trick to have the server ignore this in some cases? Maybe it's enough to do nothing in this catch case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to change the ParentId in the Initialize request when replaying the messages?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, certainly.
); | ||
cancelLanguageServer.Token.Register(() => server.ForcefulShutdown()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be good to log something here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a log message, although in another location
ab4e0e1
to
53faa7a
Compare
var logger = server.GetRequiredService<ILogger<Program>>(); | ||
logger.LogTrace("initializing service"); | ||
|
||
KillLanguageServerIfParentDies(request, cancelLanguageServer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm very new to this code, so I need to better understand it.
Why was the request not used before? Is this code run for every request?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The request is also used in another location, but this method allows you do additional work. An InitializeParams
request is sent at most once, so this code is run at most once.
This implementation strategy looks good to me. I'm surprised that the VSCode folks don't clean up language server processes when the IDE is killed. |
The LSP server process is not started by VSCode but by the LSP client NPM package that the Dafny extension uses, but that's a Microsoft package so you might be just as surprised :D There's different ways of connecting to a language server with that client though, not all of them start a fresh language server. |
var hostProcess = Process.GetProcessById((int)request.ProcessId)!; | ||
hostProcess.EnableRaisingEvents = true; | ||
hostProcess.Exited += (_, _) => Kill(); | ||
} catch { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we catch a more specific exception? And maybe shrink the try
block to just the GetProcessById?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching ArgumentException
now. Do you feel the shrinking is worth the extra verbosity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but in fact camrein suggested an even better alternative (an explicit assertion)
var error = ""; | ||
process.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs args) { error += args.Data; }; | ||
|
||
var languageServerProcessId = await process.StandardOutput.ReadLineAsync(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you wrap a custom binary that prints its PID around the server instead of just reading the PID of the regular server?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to pass the PID of a running process to the language server in the initialize request, and later kill that process. This is what the custom binary is for. It also starts the language server to make the test more realistic, since the PID is supposed to be the parent process of the language server.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then I think your solution makes sense, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, but I'm a bit confused by the testing strategy (I don't see yet why we need a custom binary to wrap the server).
.OnStarted(StartedAsync); | ||
} | ||
|
||
private static Task InitializeAsync(ILanguageServer server, InitializeParams request, CancellationToken cancellationToken) { | ||
private static Task InitializeAsync(ILanguageServer server, InitializeParams request, CancellationToken cancelRequestToken, | ||
CancellationTokenSource cancelLanguageServer) { | ||
var logger = server.GetRequiredService<ILogger<Program>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's something I must have missed when I extracted the WithDafnyLanguageServer
method into a separate class. Could you change the injected logger instance to ILogger<DafnyLanguageServer>
?
@@ -24,24 +24,23 @@ namespace Microsoft.Dafny.LanguageServer.IntegrationTest.Various { | |||
[TestClass] | |||
public class ShutdownTest { | |||
|
|||
[TestMethod] | |||
[TestMethod, Timeout(10000)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For readability, you may want to write the literal as 10_000
.
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), | ||
MetadataReference.CreateFromFile(Assembly.Load("mscorlib").Location)); | ||
MetadataReference.CreateFromFile(Assembly.Load(fileName).Location))). | ||
AddReferences( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So far I applied the convention to use the dot of method chained calls as a prefix.
AddReferences( | ||
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), | ||
MetadataReference.CreateFromFile(Assembly.Load("mscorlib").Location)); | ||
compilation = compilation.WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could continue the method chaining here since you neither explicitly name nor use the intermediate state.
1d0efcf
to
7e207d1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good so far. I've added two more comments to the others.
PS: We may switch to XUnit in the future since the rest of Dafny's test suite is built around it.
languageServer.Kill(); | ||
Assert.Fail("Language server should have killed itself if the parent is gone."); | ||
} catch (ArgumentException) { | ||
// Language server process is not running, pass the test. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a suitable assertion in MSTest:
Assert.ThrowsException<ArgumentException>(() => {
var languageServer = Process.GetProcessById(int.Parse(languageServerProcessId));
languageServer.Kill();
});
…r/dafny into issue95RunawayZ3Processes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well done for this one.
Next one interesting to solve would be the memory leak of a single DafnyLanguageServer with "save on change" that keeps too much in memory and goes to 4Gb of RAM in a few minutes.
using System.Diagnostics; | ||
using System.Threading.Tasks; | ||
|
||
public class ShortLivedProcessStarter {{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the improvements. Since I belive the use of CancellationTokenSource
is now unnecessary, I've requested one more change. After that, we're good to go.
} | ||
}); | ||
#pragma warning disable VSTHRD002 | ||
outputHandler.StopAsync().Wait(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason for this? I see that the target buffer is a MemoryStream
; thus, the actions tend to be synchronous anyway. However, it would make the warning suppression obsolete, as well as the use of Wait
that would require manual unwrapping of the nested exceptions.
55dae8e
Fixes dafny-lang/ide-vscode#95