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

Ability to kill all descendents of the child process #96

Open
callumlocke opened this issue Jun 23, 2017 · 14 comments
Open

Ability to kill all descendents of the child process #96

callumlocke opened this issue Jun 23, 2017 · 14 comments

Comments

@callumlocke
Copy link

When I'm working with child processes, I sometimes end up with detached processes running indefinitely after I've killed the direct child, which can lead to blocked ports and memory leaks.

I sometimes fix this by using a module like ps-tree to find out all the descendants PIDs, and then send all of them a kill command, but it feels messy. It would be nice if execa could provide a clean, cross-platform API for this.

Maybe something like this:

const execa = require('execa');

const child = execa('./something');

// ...

child.killTree().then(() => {
  console.log('all dead!');
});
tomsotte added a commit to tomsotte/execa that referenced this issue Jan 17, 2019
… well

This attempt to fix the problem described by the issue sindresorhus#96.
If a child process is killed, the descendents of that child process won't be killed as well. This happens on Linux but not on Windows [^1].
A solution is the "PID range hack" [^2] that uses the `detached` mode for spawning a process and then kills that child process by killing the PID group, using `process.kill(-pid)`.

*Implementation*

- added an internal option `killByPid` as a remained for the spawned child process that it will be `detached` and to kill it by PID
- expanded and moved to a separate function the routine to kill the spawned process to `killSpawned`
- the `ChildProcess#kill` method of the spawned child process will be replaced by the `killSpawned` routine, to kill by pid if necessary
- the `killSpawned` routine signals that the child process has been killed, if it has been killed by the pid

I checked and all the tests pass.

This implementation also consider the issue sindresorhus#115 and shouldn't interfere with the detached/cleanup fix.

[^1]: https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal
[^2]: https://azimi.me/2014/12/31/kill-child_process-node-js.html
tomsotte added a commit to tomsotte/execa that referenced this issue Jan 17, 2019
This is an attempt to fix the problem described by the issue sindresorhus#96.
If a child process is killed, the descendents of that child process won't be killed as well. This happens on Linux but not on Windows [^1].
The adopted solution is the "PID range hack" [^2] that uses the `detached` mode for spawning a process and then kills that child process by killing the PID group, using `process.kill(-pid)`, effectively killing all the descendents.

*Implementation*

- added an internal option `killByPid` as a remained for the spawned child process that it will be `detached` and to kill it by PID
- expanded and moved to a separate function the routine to kill the spawned process to `killSpawned`
- the `ChildProcess#kill` method of the spawned child process will be replaced by the `killSpawned` routine, to kill by pid if necessary
- the `killSpawned` routine signals that the child process has been killed, if it has been killed by the pid

I checked and all the tests pass.

This implementation also considers the issue sindresorhus#115 and shouldn't interfere with the detached/cleanup fix.

[^1]: https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal
[^2]: https://azimi.me/2014/12/31/kill-child_process-node-js.html
@tomsotte
Copy link
Contributor

Hi,

I've tried to implement a possible solution by using the pid range hack. I think it integrates in what execa already provides, which is a better child_process with cross-platform support. On Windows, when killing a process it would kill all of it descendents, but this does not happen on Linux (and apparently on macOS too).

The solution I've implemented replaces:

  • the way execa kills the spawned process for the cleanup routine
  • the ChildProcess#kill method

It shouldn't and doesn't AFAIK interfere with the detached and cleanup choice. But I'd like if someone can review and double-check it.

tomsotte added a commit to tomsotte/execa that referenced this issue Jan 20, 2019
This is an attempt to fix the problem described by the issue sindresorhus#96.
If a child process is killed, the descendents of that child process won't be killed as well. This happens on Linux but not on Windows [^1].
The adopted solution is the "PID range hack" [^2] that uses the `detached` mode for spawning a process and then kills that child process by killing the PID group, using `process.kill(-pid)`, effectively killing all the descendents.

*Implementation*

- added an internal option `killByPid` as a remained for the spawned child process that it will be `detached` and to kill it by PID
- expanded and moved to a separate function the routine to kill the spawned process to `killSpawned`
- the `ChildProcess#kill` method of the spawned child process will be replaced by the `killSpawned` routine, to kill by pid if necessary
- the `killSpawned` routine signals that the child process has been killed, if it has been killed by the pid

I checked and all the tests pass.

This implementation also considers the issue sindresorhus#115 and shouldn't interfere with the detached/cleanup fix.

[^1]: https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal
[^2]: https://azimi.me/2014/12/31/kill-child_process-node-js.html
@sindresorhus
Copy link
Owner

See: #170 (comment)

@ehmicky
Copy link
Collaborator

ehmicky commented Apr 12, 2020

Note that this is especially problematic when using shell: true.

The following:

const subprocess = execa('sleep 200', {shell: true});
subprocess.kill();
await subprocess;

Is the same as:

const subprocess = execa('/bin/sh', ['-c', 'sleep 200']);
subprocess.kill();
await subprocess;

This kills /bin/sh but not sleep 200, which is a separate process spawned by /bin/sh.

Note that both the shell: true behavior and the fact that descendant child processes are not recursively killed are not specific issues to Execa, but to core child_process.spawn() (that Execa is based upon).

@gaggle
Copy link

gaggle commented Apr 13, 2020

FWIW, I use a possible workaround (of sorts...), posting it here for others who might have the same issue and come across this thread. But.. it ain't pretty, and YMMW for its usefulness:

import execa, { ExecaChildProcess } from "execa"
import pidtree from "pidtree"

export type Execa2ChildProcess = ExecaChildProcess & { killTree: () => Promise<boolean> }

export function execa2Command (cmd: string, opts: { cwd?: string; shell?: boolean; } = {}): Execa2ChildProcess {
  const p = execa.command(cmd, opts)

  async function killTree (): Promise<boolean> {
    let pids
    try {
      pids = await pidtree(p.pid, { root: true })
    } catch (err) {
      if (err.message === "No maching pid found") return true // *
      throw err
    }
    pids.map(el => {
      try {
        process.kill(el)
      } catch (err) {
        if (err.code !== "ESRCH") throw err
      }
    })
    p.kill()
    return true
  }

  (p as any).killTree = killTree
  return p as ExecerChildProcess
}

* the error-message from pidtree is misspelled, PR simonepri/pidtree#11 attempts to fix that

This solution won't win any prizes and is maybe provably wrong in key areas, but it at least satisfies my test-suite so I can move on in my tasks.

@ehmicky
Copy link
Collaborator

ehmicky commented Apr 13, 2020

This solution (pidtree) started to be implemented in this PR. We first looked into build this into childProcess.kill(). But it turns out it was big enough that we thought this should either be its own library or be built in core Node.js itself.

@fivethreeo
Copy link

Here is a good way to do this

https://www.npmjs.com/package/terminate

const terminate = require('terminate');

const subprocess = execa("yarn", ["start"], {stdio: 'inherit', cwd: tempDir })

terminate(subprocess.pid, 'SIGINT', { timeout: 1000 }, () => {
  terminate(subprocess.pid);
});

@Kikobeats
Copy link
Contributor

Kikobeats commented Sep 27, 2021

I was using pidtree and it works pretty well, plus the dependency size is small.

const getPids = async pid => {
  const { value: pids = [] } = await pReflect(pidtree(pid))
  return pids.includes(pid) ? pids : [...pids, pid]
}

const pids = await getPids(pid)

pids.forEach(pid => {
  try {
    process.kill(pid, signal)
  } catch (_) {}

@sindresorhus
Copy link
Owner

Relevant Node.js issue: nodejs/node#40438

@kopax-polyconseil

This comment was marked as off-topic.

@ehmicky

This comment was marked as off-topic.

@Kastakin
Copy link

I was using pidtree and it works pretty well, plus the dependency size is small.

const getPids = async pid => {
  const { value: pids = [] } = await pReflect(pidtree(pid))
  return pids.includes(pid) ? pids : [...pids, pid]
}

const pids = await getPids(pid)

pids.forEach(pid => {
  try {
    process.kill(pid, signal)
  } catch (_) {}

I had good luck with this method.

I don't know if it si relevant but I only have to do this on Windows. While both on MacOS and Windows spawned processes are grouped correctly (as it can be seen by the task manager), in MacOS they are all terminated correctly when the main process is terminated, in Windows that is not the case.

@ehmicky
Copy link
Collaborator

ehmicky commented Jan 20, 2024

Although this issue has been open for almost 7 years now, this would still be a very valuable feature. Some of the points anyone implementing this should consider:

  • Cross-platform support
    • Windows and Unix have very different concepts of processes (including trees) and signals
    • The ps utility has different flags on all the following OSes/distributions: macOS, Linux, Amazon Linux, Alpine Linux, etc. Sometimes it is not even available.
  • Orphan processes. On Unix, a parent process might end while its child keeps running. When this happens, the child's new parent process becomes the init process.
    • Process groups might potentially help there, including using process.kill(-processGroup)
  • Interoperability with other Execa features.
    • Specifically, many features terminate the process: the signal option, the timeout option, the cleanup option, the maxBuffer option, and also any error event on childProcess, childProcess.stdin, childProcess.stdout or childProcess.stderr. One might want to terminate the process and its descendants in those cases.
    • One might also want the set the graceful exit timeout.
    • In the case of the cleanup option, the code will only work if synchronous.
  • PIDs are re-used by the OS, i.e. are only valid at a specific point in time.
  • If using a dependency, it needs to be maintained. Ideally, it should be small too, and well tested.

@Kikobeats
Copy link
Contributor

Kikobeats commented Jan 21, 2024

This is the way I'm doing this these days without using dependencies, just Node.js API, in the way @tomsotte (5 years ago!): https://github.com/Kikobeats/kill-process-group/blob/f32f97491f255a7b763a578c46b49da78b1485a6/src/index.js#L8-L33

@FishOrBear
Copy link

#96 (comment)

#96 (comment)
#96 (comment)

This is indeed a problem. It would be great if there is a better solution.
The above is the solution I found from the discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants