From 21ab4bad6350a52df0121cbe42dbf6b34cd7cd03 Mon Sep 17 00:00:00 2001
From: Gabriel-Ladzaretti
 <97394622+Gabriel-Ladzaretti@users.noreply.github.com>
Date: Sun, 28 Aug 2022 21:32:23 +0300
Subject: [PATCH] feat(util/exec): enable process group handling on process
 termination (#17447)

---
 docs/usage/self-hosted-experimental.md |  4 +++
 lib/util/exec/common.spec.ts           | 47 ++++++++++++++++++++++++++
 lib/util/exec/common.ts                | 34 +++++++++----------
 3 files changed, 68 insertions(+), 17 deletions(-)

diff --git a/docs/usage/self-hosted-experimental.md b/docs/usage/self-hosted-experimental.md
index c63539f9f1..f8941ad13f 100644
--- a/docs/usage/self-hosted-experimental.md
+++ b/docs/usage/self-hosted-experimental.md
@@ -58,3 +58,7 @@ If set, Renovate will enable `forcePathStyle` when instantiating the AWS s3 clie
 > Whether to force path style URLs for S3 objects (e.g., `https://s3.amazonaws.com//` instead of `https://.s3.amazonaws.com/`
 
 Source: [AWS s3 documentation - Interface BucketEndpointInputConfig](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/bucketendpointinputconfig.html)
+
+## `RENOVATE_X_EXEC_GPID_HANDLE`
+
+If set, Renovate will terminate the whole process group of a terminated child process spawned by Renovate.
diff --git a/lib/util/exec/common.spec.ts b/lib/util/exec/common.spec.ts
index 19f1bb621e..f27c087056 100644
--- a/lib/util/exec/common.spec.ts
+++ b/lib/util/exec/common.spec.ts
@@ -33,6 +33,7 @@ interface StubArgs {
   stdout?: string;
   stderr?: string;
   timeout?: number;
+  pid?: number;
 }
 
 function getReadable(
@@ -66,6 +67,7 @@ function getSpawnStub(args: StubArgs): ChildProcess {
     stderr,
     encoding,
     timeout,
+    pid = 31415,
   } = args;
   const listeners: Events = {};
 
@@ -140,6 +142,7 @@ function getSpawnStub(args: StubArgs): ChildProcess {
     emit,
     unref,
     kill,
+    pid,
   } as ChildProcess;
 }
 
@@ -282,4 +285,48 @@ describe('util/exec/common', () => {
       });
     });
   });
+
+  describe('handle gpid', () => {
+    const killSpy = jest.spyOn(process, 'kill');
+
+    afterEach(() => {
+      delete process.env.RENOVATE_X_EXEC_GPID_HANDLE;
+      jest.restoreAllMocks();
+    });
+
+    it('calls process.kill on the gpid', async () => {
+      process.env.RENOVATE_X_EXEC_GPID_HANDLE = 'true';
+      const cmd = 'ls -l';
+      const exitSignal = 'SIGTERM';
+      const stub = getSpawnStub({ cmd, exitCode: null, exitSignal });
+      spawn.mockImplementationOnce((cmd, opts) => stub);
+      killSpy.mockImplementationOnce((pid, signal) => true);
+      await expect(
+        exec(cmd, partial<RawExecOptions>({ encoding: 'utf8' }))
+      ).rejects.toMatchObject({
+        cmd,
+        signal: exitSignal,
+        message: `Command failed: ${cmd}\nInterrupted by ${exitSignal}`,
+      });
+      expect(process.kill).toHaveBeenCalledWith(-stub.pid!, exitSignal);
+    });
+
+    it('handles process.kill call on non existent gpid', async () => {
+      process.env.RENOVATE_X_EXEC_GPID_HANDLE = 'true';
+      const cmd = 'ls -l';
+      const exitSignal = 'SIGTERM';
+      const stub = getSpawnStub({ cmd, exitCode: null, exitSignal });
+      spawn.mockImplementationOnce((cmd, opts) => stub);
+      killSpy.mockImplementationOnce((pid, signal) => {
+        throw new Error();
+      });
+      await expect(
+        exec(cmd, partial<RawExecOptions>({ encoding: 'utf8' }))
+      ).rejects.toMatchObject({
+        cmd,
+        signal: exitSignal,
+        message: `Command failed: ${cmd}\nInterrupted by ${exitSignal}`,
+      });
+    });
+  });
 });
diff --git a/lib/util/exec/common.ts b/lib/util/exec/common.ts
index 3b27c5127a..4fba11a779 100644
--- a/lib/util/exec/common.ts
+++ b/lib/util/exec/common.ts
@@ -122,25 +122,25 @@ export function exec(cmd: string, opts: RawExecOptions): Promise<ExecResult> {
 
 function kill(cp: ChildProcess, signal: NodeJS.Signals): boolean {
   try {
-    // TODO: will be enabled in #16654
-    /**
-     * If `pid` is negative, but not `-1`, signal shall be sent to all processes
-     * (excluding an unspecified set of system processes),
-     * whose process group ID (pgid) is equal to the absolute value of pid,
-     * and for which the process has permission to send a signal.
-     */
-    // process.kill(-(cp.pid as number), signal);
-
-    // destroying stdio is needed for unref to work
-    // https://nodejs.org/api/child_process.html#subprocessunref
-    // https://github.com/nodejs/node/blob/4d5ff25a813fd18939c9f76b17e36291e3ea15c3/lib/child_process.js#L412-L426
-    cp.stderr?.destroy();
-    cp.stdout?.destroy();
-    cp.unref();
-    return cp.kill(signal);
+    if (cp.pid && process.env.RENOVATE_X_EXEC_GPID_HANDLE) {
+      /**
+       * If `pid` is negative, but not `-1`, signal shall be sent to all processes
+       * (excluding an unspecified set of system processes),
+       * whose process group ID (pgid) is equal to the absolute value of pid,
+       * and for which the process has permission to send a signal.
+       */
+      return process.kill(-cp.pid, signal);
+    } else {
+      // destroying stdio is needed for unref to work
+      // https://nodejs.org/api/child_process.html#subprocessunref
+      // https://github.com/nodejs/node/blob/4d5ff25a813fd18939c9f76b17e36291e3ea15c3/lib/child_process.js#L412-L426
+      cp.stderr?.destroy();
+      cp.stdout?.destroy();
+      cp.unref();
+      return cp.kill(signal);
+    }
   } catch (err) {
     // cp is a single node tree, therefore -pid is invalid as there is no such pgid,
-    // istanbul ignore next: will be covered once we use process.kill
     return false;
   }
 }
-- 
GitLab