diff --git a/package.json b/package.json index a253ffd..9d24ea1 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@types/find-root": "^1.1.2", "@types/jest": "^27.4.0", "@types/lodash": "^4.14.194", - "@types/node": "^14.14.14", + "@types/node": "^18.11.9", "@types/yargs": "^15.0.14", "eslint": "^8.40.0", "eslint-plugin-jest": "^27.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c961c5b..1e5ae60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,8 +53,8 @@ devDependencies: specifier: ^4.14.194 version: 4.14.194 '@types/node': - specifier: ^14.14.14 - version: 14.14.37 + specifier: ^18.11.9 + version: 18.11.9 '@types/yargs': specifier: ^15.0.14 version: 15.0.14 @@ -542,7 +542,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 chalk: 4.1.0 jest-message-util: 27.4.6 jest-util: 27.4.2 @@ -563,7 +563,7 @@ packages: '@jest/test-result': 27.4.6 '@jest/transform': 27.4.6 '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 ansi-escapes: 4.3.2 chalk: 4.1.0 emittery: 0.8.1 @@ -607,7 +607,7 @@ packages: dependencies: '@jest/fake-timers': 27.4.6 '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 jest-mock: 27.4.6 dev: true @@ -617,7 +617,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@sinonjs/fake-timers': 8.1.0 - '@types/node': 16.10.3 + '@types/node': 18.11.9 jest-message-util: 27.4.6 jest-mock: 27.4.6 jest-util: 27.4.2 @@ -646,7 +646,7 @@ packages: '@jest/test-result': 27.4.6 '@jest/transform': 27.4.6 '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 chalk: 4.1.0 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -730,7 +730,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 '@types/yargs': 16.0.4 chalk: 4.1.0 dev: true @@ -942,13 +942,13 @@ packages: resolution: {integrity: sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==} dependencies: '@types/minimatch': 3.0.5 - '@types/node': 16.10.3 + '@types/node': 18.11.9 dev: true /@types/graceful-fs@4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 16.10.3 + '@types/node': 18.11.9 dev: true /@types/istanbul-lib-coverage@2.0.4: @@ -990,12 +990,8 @@ packages: resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} dev: true - /@types/node@14.14.37: - resolution: {integrity: sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==} - dev: true - - /@types/node@16.10.3: - resolution: {integrity: sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==} + /@types/node@18.11.9: + resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} dev: true /@types/prettier@2.4.3: @@ -2968,7 +2964,7 @@ packages: '@jest/environment': 27.4.6 '@jest/test-result': 27.4.6 '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 chalk: 4.1.0 co: 4.6.0 dedent: 0.7.0 @@ -3091,7 +3087,7 @@ packages: '@jest/environment': 27.4.6 '@jest/fake-timers': 27.4.6 '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 jest-mock: 27.4.6 jest-util: 27.4.2 jsdom: 16.7.0 @@ -3109,7 +3105,7 @@ packages: '@jest/environment': 27.4.6 '@jest/fake-timers': 27.4.6 '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 jest-mock: 27.4.6 jest-util: 27.4.2 dev: true @@ -3125,7 +3121,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@types/graceful-fs': 4.1.5 - '@types/node': 16.10.3 + '@types/node': 18.11.9 anymatch: 3.1.3 fb-watchman: 2.0.1 graceful-fs: 4.2.9 @@ -3147,7 +3143,7 @@ packages: '@jest/source-map': 27.4.0 '@jest/test-result': 27.4.6 '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 chalk: 4.1.0 co: 4.6.0 expect: 27.4.6 @@ -3202,7 +3198,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 dev: true /jest-pnp-resolver@1.2.2(jest-resolve@27.4.6): @@ -3258,7 +3254,7 @@ packages: '@jest/test-result': 27.4.6 '@jest/transform': 27.4.6 '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 chalk: 4.1.0 emittery: 0.8.1 exit: 0.1.2 @@ -3316,7 +3312,7 @@ packages: resolution: {integrity: sha512-RDhpcn5f1JYTX2pvJAGDcnsNTnsV9bjYPU8xcV+xPwOXnUPOQwf4ZEuiU6G9H1UztH+OapMgu/ckEVwO87PwnQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: - '@types/node': 16.10.3 + '@types/node': 18.11.9 graceful-fs: 4.2.9 dev: true @@ -3355,7 +3351,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 chalk: 4.1.0 ci-info: 3.3.0 graceful-fs: 4.2.9 @@ -3380,7 +3376,7 @@ packages: dependencies: '@jest/test-result': 27.4.6 '@jest/types': 27.5.1 - '@types/node': 16.10.3 + '@types/node': 18.11.9 ansi-escapes: 4.3.2 chalk: 4.1.0 jest-util: 27.4.2 @@ -3391,7 +3387,7 @@ packages: resolution: {integrity: sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 16.10.3 + '@types/node': 18.11.9 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true diff --git a/spec/Supervisor.spec.ts b/spec/Supervisor.spec.ts index 20debe9..5a5ab82 100644 --- a/spec/Supervisor.spec.ts +++ b/spec/Supervisor.spec.ts @@ -2,6 +2,8 @@ import { ChildProcess, spawn } from "child_process"; import { range } from "lodash"; import * as path from "path"; +jest.setTimeout(10000); + const childExit = (child: ChildProcess) => { return new Promise((resolve) => { child.on("exit", (code: number) => { @@ -106,4 +108,4 @@ test("it doesn't have any stdin if wds is started with terminal commands", async await childExit(child); expect(output).toEqual(""); -}); +}); \ No newline at end of file diff --git a/src/Project.ts b/src/Project.ts index 00af5b2..afa16e2 100644 --- a/src/Project.ts +++ b/src/Project.ts @@ -58,8 +58,8 @@ export class Project { this.supervisor.restart(); } - shutdown(code = 0) { - this.supervisor.stop(); + async shutdown(code = 0) { + await this.supervisor.stop(); for (const cleanup of this.cleanups) { cleanup(); } diff --git a/src/Supervisor.ts b/src/Supervisor.ts index 4181ab8..1c63e28 100644 --- a/src/Supervisor.ts +++ b/src/Supervisor.ts @@ -1,6 +1,7 @@ +import { setTimeout } from "timers/promises"; import type { ChildProcess, StdioOptions } from "child_process"; import { spawn } from "child_process"; -import { EventEmitter } from "events"; +import { EventEmitter, once } from "events"; import type { RunOptions } from "./Options"; import type { Project } from "./Project"; import { log } from "./utils"; @@ -12,28 +13,41 @@ export class Supervisor extends EventEmitter { super(); } - stop() { - if (this.process) { - this.process.kill("SIGTERM"); + /** + * Stop the process with a graceful SIGTERM, then SIGKILL after a timeout + * Kills the whole process group so that any subprocesses of the process are also killed + * See https://azimi.me/2014/12/31/kill-child_process-node-js.html for more information + */ + async stop() { + if (!this.process || !this.process.pid) return; + + const ref = this.process; + const exit = once(ref, "exit"); + this.kill("SIGTERM"); + + await Promise.race([exit, setTimeout(5000)]); + if (!ref.killed) { + this.kill("SIGKILL", ref.pid); } - const process = this.process; - setTimeout(() => { - if (!process.killed) { - process.kill("SIGKILL"); - } - }, 5000); } - kill() { - if (this.process) { - this.process.kill("SIGKILL"); + kill(signal = "SIGKILL", pid = this.process?.pid) { + if (pid) { + try { + process.kill(-pid, signal); + } catch (error: any) { + if (error.code == "ESRCH") { + // process can't be found, is already dead + return; + } else { + throw error; + } + } } } restart() { - if (this.process) { - this.process.kill("SIGKILL"); - } + this.kill(); const stdio: StdioOptions = [null, "inherit", "inherit"]; if (!this.options.terminalCommands) { @@ -51,6 +65,7 @@ export class Supervisor extends EventEmitter { WDS_EXTENSIONS: this.project.config.extensions.join(","), }, stdio: stdio, + detached: true, }); if (this.options.terminalCommands) { diff --git a/src/index.ts b/src/index.ts index 7b65996..c792be8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -156,11 +156,11 @@ export const wds = async (options: RunOptions) => { process.on("SIGINT", () => { log.debug(`process ${process.pid} got SIGINT`); - project.shutdown(0); + void project.shutdown(0); }); process.on("SIGTERM", () => { log.debug(`process ${process.pid} got SIGTERM`); - project.shutdown(0); + void project.shutdown(0); }); project.supervisor.process.on("exit", (code) => { @@ -172,7 +172,7 @@ export const wds = async (options: RunOptions) => { return; } logShutdown("shutting down project since it's no longer needed..."); - project.shutdown(code ?? 1); + void project.shutdown(code ?? 1); }); return server;