From d86a41aa52d6779ecfab753f027556d7fcdfbdec Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 21 Jun 2023 19:36:37 -0400 Subject: [PATCH] fix(dev): wait until all app server descendants have been killed (#6663) --- .changeset/blue-dots-glow.md | 5 ++ .../remix-dev/devServer_unstable/index.ts | 29 ++--------- packages/remix-dev/devServer_unstable/proc.ts | 48 +++++++++++++++++++ packages/remix-dev/package.json | 1 + yarn.lock | 5 ++ 5 files changed, 64 insertions(+), 24 deletions(-) create mode 100644 .changeset/blue-dots-glow.md create mode 100644 packages/remix-dev/devServer_unstable/proc.ts diff --git a/.changeset/blue-dots-glow.md b/.changeset/blue-dots-glow.md new file mode 100644 index 00000000000..b96818f6907 --- /dev/null +++ b/.changeset/blue-dots-glow.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +fix `remix dev -c`: kill all descendant processes of specified command when restarting diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts index d01d0aaa616..25cebb061d9 100644 --- a/packages/remix-dev/devServer_unstable/index.ts +++ b/packages/remix-dev/devServer_unstable/index.ts @@ -22,6 +22,7 @@ import type { Result } from "../result"; import { err, ok } from "../result"; import invariant from "../invariant"; import { logger } from "../tux"; +import { kill, killtree } from "./proc"; type Origin = { scheme: string; @@ -214,7 +215,9 @@ export let serve = async ( try { let start = Date.now(); if (state.appServer === undefined || options.restart) { - await kill(state.appServer); + if (state.appServer?.pid) { + await killtree(state.appServer.pid); + } state.appServer = startAppServer(options.command); } let appReady = await state.appReady!.result; @@ -276,7 +279,7 @@ export let serve = async ( server.listen(origin.port); return new Promise(() => {}).finally(async () => { - await kill(state.appServer); + state.appServer?.pid && (await kill(state.appServer.pid)); websocket.close(); server.close(); await dispose(); @@ -290,25 +293,3 @@ let clean = (config: RemixConfig) => { }; let relativePath = (file: string) => path.relative(process.cwd(), file); - -let kill = async (p?: execa.ExecaChildProcess) => { - if (p === undefined) return; - let channel = Channel.create(); - p.on("exit", channel.ok); - - // https://github.com/nodejs/node/issues/12378 - if (process.platform === "win32") { - try { - await execa("taskkill", ["/pid", String(p.pid), "/f", "/t"]); - } catch (error) { - // if exit code is 128, app server process is already dead - if (!(error instanceof Error)) throw error; - if (!("exitCode" in error)) throw error; - if (error.exitCode !== 128) throw error; - } - } else { - p.kill("SIGTERM", { forceKillAfterTimeout: 1_000 }); - } - - await channel.result; -}; diff --git a/packages/remix-dev/devServer_unstable/proc.ts b/packages/remix-dev/devServer_unstable/proc.ts new file mode 100644 index 00000000000..47b8c6f0170 --- /dev/null +++ b/packages/remix-dev/devServer_unstable/proc.ts @@ -0,0 +1,48 @@ +import execa from "execa"; +import pidtree from "pidtree"; + +let isWindows = process.platform === "win32"; + +export let kill = async (pid: number) => { + try { + let cmd = isWindows + ? ["taskkill", "/F", "/PID", pid.toString()] + : ["kill", "-9", pid.toString()]; + await execa(cmd[0], cmd.slice(1)); + } catch (error) { + throw new Error(`Failed to kill process ${pid}: ${error}`); + } +}; + +let isAlive = (pid: number) => { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return false; + } +}; + +export let killtree = async (pid: number) => { + let descendants = await pidtree(pid); + let pids = [pid, ...descendants]; + + await Promise.all(pids.map(kill)); + + return new Promise((resolve, reject) => { + let check = setInterval(() => { + let alive = pids.filter(isAlive); + if (alive.length === 0) { + clearInterval(check); + resolve(); + } + }, 50); + + setTimeout(() => { + clearInterval(check); + reject( + new Error("Timeout: Processes did not exit within the specified time.") + ); + }, 2000); + }); +}; diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 9303588aa6d..681d5f3f342 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -54,6 +54,7 @@ "ora": "^5.4.1", "picocolors": "^1.0.0", "picomatch": "^2.3.1", + "pidtree": "^0.6.0", "postcss": "^8.4.19", "postcss-discard-duplicates": "^5.1.0", "postcss-load-config": "^4.0.1", diff --git a/yarn.lock b/yarn.lock index e226ee9eb70..1a1f4d04daa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10947,6 +10947,11 @@ pidtree@^0.3.0: resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz" integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== +pidtree@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"