Skip to content

fix: Servers terminating even with 'detached: true' #1603

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

nkomonen-amazon
Copy link

@nkomonen-amazon nkomonen-amazon commented Jan 28, 2025

Background Info

  • In Unix-like environments, the child process would sometimes not exit even when the parent did.
  • The exit/watchdog timers were created due to address this behavior. See the docstring of setupExitTimer().
  • The 'detached' flag would not be honored in Unix-like environments, and the process to stay running in some cases.

Problem

If a language server was set as detached in the ServerOptions, and we wanted it to not exit on parent exit, it would
still exit.

Solution

Commits in this PR have been split up in to smaller concerns, see their comments for specific changes.

Summary is that when the detached flag is set in the ServerOptions, it will now ensure that the Server actually stays alive when the parent exits.

Testing

  • Wasn't able to find an easy way to write a test for behavior when the parent (client) terminates and that the child continues to run.
  • Created an E2E test that the server gets the "detached" flag forwarded to it by the client
  • I manually tested by doing the following:
    • Have the test bed client log the ExtHost PID: console.log('Client PID: ${process.pid}')
    • Have the test bed server log its PID, such as a through a notification to the client
    • Run the test bed
    • Configure the detached flag in the ServerOptions
    • Kill the client PID
    • Observe that the child PID continues running if detached: true, otherwise it should not exist if detached: false

Manually tested on:

  • MacOS
  • Windows
  • Linux

Behaviors

  • By default detached: false
    • Server will terminate from either an exit notification, or the parent exit/watchdog timers
  • When detached: true & detachedTimeout is set THEN the server will timeout after detachedTimeout milliseconds have passed since the parent (client) have terminated
  • When detached: true, a graceful shutdown of the VS Code application results in the Language Server still running
    • BUT if the client was pushed to context.subscriptions it will be cleaned up regardless of detached
    • QUESTION: Should this behavior be documented, and where?

Resolves #1595

Signed-off-by: nkomonen-amazon [email protected]

@nkomonen-amazon nkomonen-amazon changed the title Detached fix fix: Servers terminating even with 'detached: true' Jan 28, 2025
@nkomonen-amazon
Copy link
Author

@microsoft-github-policy-service agree company="Amazon"

// channel (e.g. IPC) disconnects, the child terminates.
process.on('disconnect', () => {});

// Keeps the server alive since node may terminate this process if
Copy link
Member

Choose a reason for hiding this comment

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

I would not hardcode this here. Could we pass that as a argument to the --detach param.

Copy link
Author

Choose a reason for hiding this comment

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

Good point, I've added an optional value for --detached NNNN to set the timeout in milliseconds.

@nkomonen-amazon
Copy link
Author

nkomonen-amazon commented Feb 11, 2025

I was able to get a Windows machine, I'm going to do some testing on it
Tested on Windows and things lgtm.

I've added some more information under the Behaviors section of the PR about how this change will impact things

@nkomonen-amazon
Copy link
Author

Hey @dbaeumer would you be able to give this another look

We have a couple places we parse cli args that have a value,
so a general utility will help deduplicate code

Signed-off-by: nkomonen-amazon <[email protected]>
Background Info:

- In Unix-like environments, the child process would sometimes not exit even when the parent did.
- The exit/watchdog timers were created due to address this behavior. See the docstring of setupExitTimer().
- The 'detached' flag would not be honored in Unix-like environments, and looks to stay open regardless in some cases.

Problem:

If a language server was set as detached in the ServerOptions, and we wanted it to not exit on parent exit, it would
still exit due to the setupExitTimer()

Solution:

- A server will know that it is detached by the cli process argument: '--detached'
 - The client side code to inject this is in a following commit
- The exit/watchdog timers will not run if the detached flag is true
- When the input stream to the server ends due to parent terminating, the server would also. This PR
  changes it to skip this if detached.

Signed-off-by: nkomonen-amazon <[email protected]>
- IMPORTANT: We need the setInterval for detached to work, so that the server
  does not auto-terminate when the parent does. See the code comment which explains why.

- By default if the user specifies for the language server to be detached,
  it will never quit after the parent/client terminates.

  But if a number (milliseconds) is provided when launching the
  server it will timeout after that amount of time, after the client terminates.

Signed-off-by: nkomonen-amazon <[email protected]>
All different methods that can support detached (i.e the server will not
terminate when the parent does) will now do so.

- A new field has been added in for the ForkOptions so that forked servers
  can now be detached.
- Only MessageTransport and StreamInfo will remain the same. I have not looked in to how those
  would support properly detaching.

Signed-off-by: nkomonen-amazon <[email protected]>
- Allows the client to configure a ServerOptions setting
that sets the max lifetime of the detached language server
after the parent/client has terminated.

- Additionally, this exposes some utils from the language server
  so that they can be reused in tests. Otherwise we'd have to duplicate
  code and reduce code coverage

Signed-off-by: nkomonen-amazon <[email protected]>
Copy link
Member

@dbaeumer dbaeumer left a comment

Choose a reason for hiding this comment

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

Great work. Only small things.

@@ -57,10 +57,18 @@ namespace Transport {
}
}

export interface ExecutableOptions {
export interface DetachedOptions {
Copy link
Member

Choose a reason for hiding this comment

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

I would do this differently. We should define detached as detached: boolean | number. If it is a boolean then we should use a default timeout. If it is a number we use it and -1 represent indefinite. This makes it more in line with other timeout settings.

* This function assumes the parent process has been properly configured with
* necessary settings (e.g., using `child_process.unref()`).
*/
function isDetached() {
Copy link
Member

Choose a reason for hiding this comment

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

Small things: declare as return a boolean

// the parent terminates + this process has nothing in the event loop.
setInterval(() => {
// Check if the detached server has reached the user-specified lifetime
if (_protocolConnectionEndTime && timeoutMillis && Date.now() - _protocolConnectionEndTime >= timeoutMillis) {
Copy link
Member

Choose a reason for hiding this comment

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

Although not happening here I usually try to make undefined checks explicit especially if the variable as a or type that can falsify.

_protocolConnectionEndTime !== undefined

@@ -24,6 +24,10 @@
"./browser": {
"types": "./lib/browser/main.d.ts",
"browser": "./lib/browser/main.js"
},
"./utils": {
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 we need this. It will export this from the npm package and will force use to keep it stable as API.

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.

detached not working as expected
2 participants