diff --git a/lib/Server.js b/lib/Server.js index 6a79d51701..caab264390 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -32,6 +32,7 @@ class Server { this.options = options; this.staticWatchers = []; + this.listeners = []; // Keep track of websocket proxies for external websocket upgrade. this.webSocketProxies = []; this.sockets = []; @@ -1168,15 +1169,11 @@ class Server { let needForceShutdown = false; - const exitProcess = () => { - // eslint-disable-next-line no-process-exit - process.exit(); - }; - signals.forEach((signal) => { - process.on(signal, () => { + const listener = () => { if (needForceShutdown) { - exitProcess(); + // eslint-disable-next-line no-process-exit + process.exit(); } this.logger.info( @@ -1187,12 +1184,20 @@ class Server { this.stopCallback(() => { if (typeof this.compiler.close === "function") { - this.compiler.close(exitProcess); + this.compiler.close(() => { + // eslint-disable-next-line no-process-exit + process.exit(); + }); } else { - exitProcess(); + // eslint-disable-next-line no-process-exit + process.exit(); } }); - }); + }; + + this.listeners.push({ name: signal, listener }); + + process.on(signal, listener); }); } @@ -1712,22 +1717,25 @@ class Server { ); } - runBonjour() { - const bonjour = require("bonjour")(); + stopBonjour(callback = () => {}) { + this.bonjour.unpublishAll(() => { + this.bonjour.destroy(); + + if (callback) { + callback(); + } + }); + } - bonjour.publish({ + runBonjour() { + this.bonjour = require("bonjour")(); + this.bonjour.publish({ name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`, port: this.options.port, type: this.options.server.type === "http" ? "http" : "https", subtypes: ["webpack"], ...this.options.bonjour, }); - - process.on("exit", () => { - bonjour.unpublishAll(() => { - bonjour.destroy(); - }); - }); } logStatus() { @@ -2198,13 +2206,21 @@ class Server { } } - startCallback(callback) { + startCallback(callback = () => {}) { this.start() .then(() => callback(null), callback) .catch(callback); } async stop() { + if (this.bonjour) { + await new Promise((resolve) => { + this.stopBonjour(() => { + resolve(); + }); + }); + } + this.webSocketProxies = []; await Promise.all(this.staticWatchers.map((watcher) => watcher.close())); @@ -2258,9 +2274,15 @@ class Server { this.middleware = null; } } + + // We add listeners to signals when creating a new Server instance + // So ensure they are removed to prevent EventEmitter memory leak warnings + for (const item of this.listeners) { + process.removeListener(item.name, item.listener); + } } - stopCallback(callback) { + stopCallback(callback = () => {}) { this.stop() .then(() => callback(null), callback) .catch(callback); diff --git a/test/e2e/bonjour.test.js b/test/e2e/bonjour.test.js index 8f93c9c15f..92c341fdef 100644 --- a/test/e2e/bonjour.test.js +++ b/test/e2e/bonjour.test.js @@ -8,8 +8,17 @@ const runBrowser = require("../helpers/run-browser"); const port = require("../ports-map").bonjour; describe("bonjour option", () => { - const mockPublish = jest.fn(); - const mockUnpublishAll = jest.fn(); + let mockPublish; + let mockUnpublishAll; + let mockDestroy; + + beforeEach(() => { + mockPublish = jest.fn(); + mockUnpublishAll = jest.fn((callback) => { + callback(); + }); + mockDestroy = jest.fn(); + }); describe("as true", () => { let compiler; @@ -24,6 +33,7 @@ describe("bonjour option", () => { return { publish: mockPublish, unpublishAll: mockUnpublishAll, + destroy: mockDestroy, }; }); @@ -42,8 +52,10 @@ describe("bonjour option", () => { afterEach(async () => { await browser.close(); await server.stop(); + mockPublish.mockReset(); mockUnpublishAll.mockReset(); + mockDestroy.mockReset(); }); it("should call bonjour with correct params", async () => { @@ -69,6 +81,7 @@ describe("bonjour option", () => { }); expect(mockUnpublishAll).toHaveBeenCalledTimes(0); + expect(mockDestroy).toHaveBeenCalledTimes(0); expect(response.status()).toMatchSnapshot("response status"); @@ -93,6 +106,7 @@ describe("bonjour option", () => { return { publish: mockPublish, unpublishAll: mockUnpublishAll, + destroy: mockDestroy, }; }); @@ -111,8 +125,10 @@ describe("bonjour option", () => { afterEach(async () => { await browser.close(); await server.stop(); + mockPublish.mockReset(); mockUnpublishAll.mockReset(); + mockDestroy.mockReset(); }); it("should call bonjour with 'https' type", async () => { @@ -138,6 +154,7 @@ describe("bonjour option", () => { }); expect(mockUnpublishAll).toHaveBeenCalledTimes(0); + expect(mockDestroy).toHaveBeenCalledTimes(0); expect(response.status()).toMatchSnapshot("response status"); @@ -162,6 +179,7 @@ describe("bonjour option", () => { return { publish: mockPublish, unpublishAll: mockUnpublishAll, + destroy: mockDestroy, }; }); @@ -180,8 +198,10 @@ describe("bonjour option", () => { afterEach(async () => { await browser.close(); await server.stop(); + mockPublish.mockReset(); mockUnpublishAll.mockReset(); + mockDestroy.mockReset(); }); it("should call bonjour with 'https' type", async () => { @@ -207,6 +227,7 @@ describe("bonjour option", () => { }); expect(mockUnpublishAll).toHaveBeenCalledTimes(0); + expect(mockDestroy).toHaveBeenCalledTimes(0); expect(response.status()).toMatchSnapshot("response status"); @@ -231,6 +252,7 @@ describe("bonjour option", () => { return { publish: mockPublish, unpublishAll: mockUnpublishAll, + destroy: mockDestroy, }; }); @@ -258,8 +280,10 @@ describe("bonjour option", () => { afterEach(async () => { await browser.close(); await server.stop(); + mockPublish.mockReset(); mockUnpublishAll.mockReset(); + mockDestroy.mockReset(); }); it("should apply bonjour options", async () => { @@ -286,6 +310,7 @@ describe("bonjour option", () => { }); expect(mockUnpublishAll).toHaveBeenCalledTimes(0); + expect(mockDestroy).toHaveBeenCalledTimes(0); expect(response.status()).toMatchSnapshot("response status"); @@ -310,6 +335,7 @@ describe("bonjour option", () => { return { publish: mockPublish, unpublishAll: mockUnpublishAll, + destroy: mockDestroy, }; }); @@ -338,8 +364,10 @@ describe("bonjour option", () => { afterEach(async () => { await browser.close(); await server.stop(); + mockPublish.mockReset(); mockUnpublishAll.mockReset(); + mockDestroy.mockReset(); }); it("should apply bonjour options", async () => { @@ -366,6 +394,7 @@ describe("bonjour option", () => { }); expect(mockUnpublishAll).toHaveBeenCalledTimes(0); + expect(mockDestroy).toHaveBeenCalledTimes(0); expect(response.status()).toMatchSnapshot("response status"); @@ -390,6 +419,7 @@ describe("bonjour option", () => { return { publish: mockPublish, unpublishAll: mockUnpublishAll, + destroy: mockDestroy, }; }); @@ -402,7 +432,9 @@ describe("bonjour option", () => { type: "http", protocol: "udp", }, - server: "https", + server: { + type: "https", + }, }, compiler ); @@ -418,6 +450,7 @@ describe("bonjour option", () => { afterEach(async () => { await browser.close(); await server.stop(); + mockPublish.mockReset(); mockUnpublishAll.mockReset(); }); @@ -446,6 +479,7 @@ describe("bonjour option", () => { }); expect(mockUnpublishAll).toHaveBeenCalledTimes(0); + expect(mockDestroy).toHaveBeenCalledTimes(0); expect(response.status()).toMatchSnapshot("response status");