Skip to content
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

Merged

Conversation

keyboardDrummer
Copy link
Member

}
catch {
// If the process dies before we get here then request shutdown immediately
cancelLanguageServer.Cancel();
Copy link
Member

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?

Copy link
Member Author

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?

Copy link
Member

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());
Copy link
Member

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.

Copy link
Member Author

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

var logger = server.GetRequiredService<ILogger<Program>>();
logger.LogTrace("initializing service");

KillLanguageServerIfParentDies(request, cancelLanguageServer);
Copy link
Member

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?

Copy link
Member Author

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.

@cpitclaudel
Copy link
Member

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.

@keyboardDrummer
Copy link
Member Author

keyboardDrummer commented Dec 23, 2021

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 {
Copy link
Member

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?

Copy link
Member Author

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?

Copy link
Member

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();
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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!

Copy link
Member

@cpitclaudel cpitclaudel left a 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>>();
Copy link
Member

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)]
Copy link
Member

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(
Copy link
Member

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));
Copy link
Member

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.

Copy link
Member

@camrein camrein left a 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.
Copy link
Member

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();
});

fabiomadge
fabiomadge previously approved these changes Dec 27, 2021
MikaelMayer
MikaelMayer previously approved these changes Dec 27, 2021
Copy link
Member

@MikaelMayer MikaelMayer left a 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 {{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this name.

Copy link
Member

@camrein camrein left a 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();
Copy link
Member

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.

Source/DafnyLanguageServer/Program.cs Outdated Show resolved Hide resolved
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Runaway Z3 processes
5 participants