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 child processes not being killed properly #613

Merged

Conversation

vpyatnitskiy
Copy link
Contributor

Currently, headless LSP processes continue running when VS Code exits on Linux.

Simple steps to reproduce:

  1. Launch VS Code.

  2. Check the headless processes ― for example, with ps ajx | grep headless.
    Here's the output on my machine:

    vpyatnitskiy@vpyatnitskiy-katana:~/tmp/godot-vscode-plugin$ ps ajx | grep headless
      26910   27154   27154   27154 ?             -1 Ss    1000   0:00 /bin/sh -c "/home/vpyatnitskiy/.local/share/Steam/steamapps/common/Godot Engine/godot.x11.opt.tools.64" --path "/home/vpyatnitskiy/godot-projects/Left or Right" --editor --headless --no-window --lsp-port 38661
      27154   27159   27154   27154 ?             -1 Rl    1000   0:01 /home/vpyatnitskiy/.local/share/Steam/steamapps/common/Godot Engine/godot.x11.opt.tools.64 --path /home/vpyatnitskiy/godot-projects/Left or Right --editor --headless --no-window --lsp-port 38661
       2292   27235   27234    2292 pts/1      27234 S+    1000   0:00 grep --color=auto headless
    
  3. Close VS Code.

  4. Check the processes again. Here's my output:

    vpyatnitskiy@vpyatnitskiy-katana:~/tmp/godot-vscode-plugin$ ps ajx | grep headless
          1   27159   27154   27154 ?             -1 Sl    1000   0:03 /home/vpyatnitskiy/.local/share/Steam/steamapps/common/Godot Engine/godot.x11.opt.tools.64 --path /home/vpyatnitskiy/godot-projects/Left or Right --editor --headless --no-window --lsp-port 38661
       2292   27290   27289    2292 pts/1      27289 R+    1000   0:00 grep --color=auto headless
    

As you can see, the shell process (/bin/sh) is gone, but the engine itself continues running. Each of those is half a gig of RAM ― relaunch a few times and it adds up.

What happens here is:

  • shell: true causes child_process.spawn() to start a shell process which, in turn, starts Godot Engine. The returned PID is the shell's PID.

  • Child processes are allowed to persist even when detached (see Node.js documentation on options.detached) ― so process.kill()ing the shell does not kill the engine.

Now, options.detached in this case makes Node start a new process group with the shell process as its leader; to terminate everything, the entire group needs to be killed. In the snippets above, you can see the group ID in the third column.

Process group IDs in Linux are equal to the PIDs of their leaders; to send a signal to a group, negative values for PID are used. See e.g. this answer on StackOverflow.

This PR makes it so that the kill signal is indeed dispatched to the group. It's a single character change, but I feel like it deserved a detailed explanation.

On Linux, child processes are allowed to persist when their parent is terminated.

Currently, headless LSP processes are spawned with `shell: true` option, which
means a shell process is launched first, and its PID is returned. Calling
`process.kill()` on that does kill the shell process, but the Godot Engine
instance launched by the shell keeps running.

Instead, kill the process group of which the shell is the leader (guaranteed by
the `detached: true` option). In Linux, process group IDs are equal to PIDs
of their leader processes, and negative PIDs are used to send a signal to a group.
Thus, negating the PID in `process.kill()` call is enough.
@DaelonSuzuka
Copy link
Collaborator

I had that minus sign at one point and I vaguely recall that it caused a problem on either Windows or MacOS.

I'll have to look into this to remember why I removed it.

@vpyatnitskiy
Copy link
Contributor Author

Sure. If I'm reading this correctly, the else branch should not execute on Windows nor MacOS, so it's possible that it was masking an unrelated problem.

@LeoDog896
Copy link
Contributor

LeoDog896 commented Mar 11, 2024

This was a slight dig, but after finding the originating pull request on the change (#452)
image

DaelonSuzuka@557fb29
DaelonSuzuka@c8ccef5
DaelonSuzuka@446b6f1

There appeared to be an issue with using the group PID - perhaps it didn't work at all?

If I get to it, I'll also try investigating that in Linux.

@vpyatnitskiy
Copy link
Contributor Author

vpyatnitskiy commented Mar 11, 2024

I see. That seems to be debugger-related? Both the debugger and the headless LSP use the same utils/subspawn.ts routines to manage child processes, and the commits you linked seem to be surrounded by other work done on the debugger.

There's quite a lot going on in the linked PR, and I don't seem to find any discussion related to the linked commits.

However, there's in fact a similar ongoing issue with the debugger where the process under debug is not being terminated when the debugging session closes. It reproduces very reliably on my machine. It might be that the commits above were an attempt to fix this.

I wasn't able to locate the cause a week ago when I prepared this PR, but I took a second look just now, and this line seems to be the cause:

const debugProcess = subProcess("debug", command, { shell: true });

This call makes the debugged process unkillable, because shell: true makes it run under a different PID, while the lack of detached: true also makes it run under its own, separate process group.

I tested on my machine, and can confirm that adding detached: true (combined with the minus sign from this PR) resolves the debugger problem.

I'm not in love with the idea of adding the fix to this PR, or other PR for that matter, because I'm not sure what other things I should test. What do you think?

@LeoDog896
Copy link
Contributor

In that case, since this PR doesn't break the debugger's ability to properly kill its own process (as it is already broken), it seems fine to merge with an additional PR (tested by @DaelonSuzuka) that adds the detached header.

vpyatnitskiy added a commit to vpyatnitskiy/godot-vscode-plugin that referenced this pull request Mar 11, 2024
See discussion near godotengine#613 (comment) .

Currently, the debugger process is spawned with `shell: true` option, which
means a shell process is launched first, and its PID is returned.

When a debugging session terminates, `godot-tools` attempts to kill the process.
However, on Linux, calling `process.kill()` on that kills the shell process,
while allowing the main process to keep running.

To fix, enable `detached: true` flag when launching the debugger to force
both processes to share the same process group; then rely on changes
introduced in godotengine#613
to kill the entire group.
@vpyatnitskiy
Copy link
Contributor Author

OK, I've sent a separate PR for this. Thank you!

@DaelonSuzuka
Copy link
Collaborator

@vpyatnitskiy Thanks for catching this problem and submitting a fix.

@DaelonSuzuka DaelonSuzuka changed the title Properly kill headless LSP process on Linux Fix child processes not being killed properly Mar 11, 2024
@DaelonSuzuka DaelonSuzuka merged commit 5cef963 into godotengine:master Mar 11, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants