From 745c05b35550883f3dd3509746a5d297878081e3 Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Mon, 21 Feb 2022 17:17:34 +0100 Subject: [PATCH 01/10] Feat: Kill child processes recursively --- README.md | 2 + package.json | 4 +- src/commands/start.command.ts | 115 ++++++++++++++++++++++++++++------ src/ecosystem.config.ts | 12 ---- src/index.ts | 7 +-- tsconfig.json | 1 + 6 files changed, 105 insertions(+), 36 deletions(-) delete mode 100644 src/ecosystem.config.ts diff --git a/README.md b/README.md index 73332c3..9e2b3b6 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # @comet/dev-process-manager + +## Usage diff --git a/package.json b/package.json index 3e8bf76..ec94c79 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ }, "devDependencies": { "@comet/eslint-config": "^0.0.1-canary.8102.6588", - "typescript": "^4.5.5", + "eslint": "^7.0.0", "ts-loader": "^8.0.0", - "eslint": "^7.0.0" + "typescript": "^4.5.5" }, "engines": { "node": "14" diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index 8e69685..a757626 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -2,7 +2,8 @@ import { spawn, ChildProcess } from "child_process"; import { Socket, createServer, createConnection } from "net"; import { AppDefinition } from "../app-definition.type"; -export const start = async (apps: AppDefinition[]) => { +export const start = async (pmConfigFilePath: string) => { + const { apps }: { apps: AppDefinition[] } = await import(`${process.cwd()}/${pmConfigFilePath}`); const processes: { [key: string]: ChildProcess } = {}; const logSockets: { socket: Socket, name: string | null; }[] = []; let shuttingDown = false; @@ -14,7 +15,7 @@ export const start = async (apps: AppDefinition[]) => { process.stdout.write(data); logSockets.forEach(s => { if (!s.name || s.name == app.name) { - s.socket.write(data); + s.socket.write(`${s.name}: ${data}`); } }); }); @@ -22,7 +23,7 @@ export const start = async (apps: AppDefinition[]) => { process.stderr.write(data); logSockets.forEach(s => { if (!s.name || s.name == app.name) { - s.socket.write(data); + s.socket.write(`${s.name}: ${data}`); } }); }); @@ -97,13 +98,7 @@ export const start = async (apps: AppDefinition[]) => { s.write(JSON.stringify(response)); s.end(); } else if (cmd == "shutdown") { - console.log("shutting down"); - shuttingDown = true; - Object.values(processes).forEach(p => { - if (!p.killed) p.kill("SIGINT"); - }) - server.close(); - process.exit(); + shutdown(s); } else { console.error("Unknown command", cmd); } @@ -115,15 +110,99 @@ export const start = async (apps: AppDefinition[]) => { }); process.on("SIGINT", function () { - console.log("shutting down") - server.close(); + shutdown(); + }); + + process.on("SIGTERM", function () { + shutdown(); + }); + + const events = ["beforeExit", "disconnected", "message", "rejectionHandled", "uncaughtException", "exit", "SIGABRT", "SIGHUP", "SIGPWR", "SIGQUIT"]; + + events.forEach((eventName) => { + process.on(eventName, (...args) => { + console.log('Unhandled error event ' + eventName + ' was called with args : ' + args.join(',')); + shutdown(); + }); + }); + + + const shutdown = async (s?: Socket) => { + console.log("shutting down"); shuttingDown = true; - for (const name in processes) { - const p = processes[name]; - if (!p.killed) { - console.log("killing " + name); - p.kill("SIGINT"); + await Promise.all(Object.values(processes).map(async p => { + if (p.pid) { + const list: { [key: string]: string[] } = await getChildProcesses(p.pid.toString(), { [p.pid]: [] }, { [p.pid]: 1 }); + killProcesses(list); } + })); + server.close(); + s?.destroy(); + process.exit(); + } + + const killProcesses = (tree: { [key: string]: string[] }) => { + const killed: { [key: string]: number } = {}; + Object.keys(tree).map((pid) => { + tree[pid].map((childPid) => { + if (!killed[childPid]) { + killPid(childPid); + killed[childPid] = 1; + } + if (!killed[pid]) { + killPid(pid); + killed[pid] = 1; + } + }) + }) + return; + } + + const killPid = (pid: string) => { + try { + process.kill(parseInt(pid, 10), "SIGINT"); + } catch (err) { + console.error(err); } - }); + } + + const getChildProcesses = async (parentPid: string, tree: { [key: string]: string[] }, pidsToProcess: { [key: string]: number }): Promise<{ [key: string]: string[] }> => { + return new Promise((resolve, reject) => { + const ps = spawn('pgrep', ['-P', parentPid]); + let allData = ""; + + const onClose = (code: unknown) => { + delete pidsToProcess[parentPid]; + if (code != 0) { + if (Object.keys(pidsToProcess).length === 0) { + resolve(tree); + } + return tree; + } + + const pids = allData.match(/\d+/g) || []; + if (pids.length === 0) { + return resolve(tree); + } + pids.forEach(function (pid) { + tree[parentPid].push(pid); + tree[pid] = []; + pidsToProcess[pid] = 1; + resolve(getChildProcesses(pid, tree, pidsToProcess)); + }) + }; + + ps.on('error', function (err) { + console.error(err); + reject(err); + }); + + ps.stdout.on('data', function (data) { + data = data.toString('ascii'); + allData += data; + }) + + ps.on('close', onClose); + }) + } } diff --git a/src/ecosystem.config.ts b/src/ecosystem.config.ts deleted file mode 100644 index 0fb1fbb..0000000 --- a/src/ecosystem.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default { - apps: [ - { - name: "sleep60", - script: "echo sleep60 && sleep 60", - }, - { - name: "sleep3", - script: "while true; do echo sleep3 && sleep 3; done", - }, - ] -}; diff --git a/src/index.ts b/src/index.ts index f1db71c..6044509 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,10 @@ import { Command } from 'commander'; import { start, shutdown, status, logs, restart } from "./commands"; -import eco from "./ecosystem.config"; const program = new Command(); -program.command("start") - .action(() => { - start(eco.apps); +program.command("start ") + .action((pmConfigFilePath) => { + start(pmConfigFilePath); }); program.command("logs [name]") diff --git a/tsconfig.json b/tsconfig.json index 51f7eb3..b2e311d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "strict": true, "strictPropertyInitialization": false, "strictNullChecks": true, + "resolveJsonModule": true, }, "include": [ "src" From 590a48cc047ddf48e99bf50dfbffc02dc3328141 Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Mon, 21 Feb 2022 17:24:15 +0100 Subject: [PATCH 02/10] Feat: Handle not deleted .pm.sock --- src/commands/start.command.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index a757626..7285b44 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -1,6 +1,7 @@ import { spawn, ChildProcess } from "child_process"; import { Socket, createServer, createConnection } from "net"; import { AppDefinition } from "../app-definition.type"; +import { unlinkSync } from "fs"; export const start = async (pmConfigFilePath: string) => { const { apps }: { apps: AppDefinition[] } = await import(`${process.cwd()}/${pmConfigFilePath}`); @@ -41,6 +42,10 @@ export const start = async (pmConfigFilePath: string) => { processes[app.name] = p; } + try { + unlinkSync("./.pm.sock"); + } catch (e) { } + const server = createServer(); server.listen(".pm.sock"); server.on('connection', (s) => { From c11eaaefa6648172a5dbf8b6ce7100c8c4980893 Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Tue, 22 Feb 2022 09:13:00 +0100 Subject: [PATCH 03/10] Docs: Readme with installation and usage --- README.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/README.md b/README.md index 9e2b3b6..59bcc32 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,77 @@ # @comet/dev-process-manager +## Installation + +```console +$ npm install +$ npm run build +``` + ## Usage + +Add `pm.config.js` file to the project root. +This file defines all available apps, which should be started by dev-process-manager. + +### pm.config.js + +```javascript +module.exports = { + apps: [ + { + name: "api", + script: "npm --prefix api install && dotenv -- wait-on -l tcp:$POSTGRESQL_PORT && npm run --prefix api fixtures:dev && npm --prefix api run start:dev", + }, + ... + ], +}; + +``` + +Update dev script in package.json + +### package.json + +```json + ... + "dev": "dotenv -- node /lib/index.js start pm.config.js", + ... +``` + +## Commands + +### Start +Either use the package.json script and run `npm run dev` +or start with: +```console +$ node /lib/index.js start +``` + +### Shutdown + +Shutdown all running apps +```console +$ node /lib/index.js shutdown +``` + +### Restart + +Restart a previously started apps + +```console +$ node /lib/index.js restart +``` + + +### Status +Lists running apps + +```console +$ node /lib/index.js status +``` + +### Logs +Prints logs of either a specific app or all running apps in real time. + +```console +$ node /lib/index.js logs [app-name] +``` From f36de4d88dd52852e1ebb920c6233ad567735f16 Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Fri, 1 Apr 2022 12:39:47 +0200 Subject: [PATCH 04/10] Feat: Throw error instead of removing the .pm.sock file at start --- src/commands/start.command.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index 7285b44..e462d11 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -1,7 +1,7 @@ import { spawn, ChildProcess } from "child_process"; import { Socket, createServer, createConnection } from "net"; import { AppDefinition } from "../app-definition.type"; -import { unlinkSync } from "fs"; +import { existsSync } from "fs"; export const start = async (pmConfigFilePath: string) => { const { apps }: { apps: AppDefinition[] } = await import(`${process.cwd()}/${pmConfigFilePath}`); @@ -43,7 +43,10 @@ export const start = async (pmConfigFilePath: string) => { } try { - unlinkSync("./.pm.sock"); + if (existsSync("./.pm.sock")) { + console.log("Could not start dev-pm server. A '.pm.sock' file already exists. \nThere are 2 possible reasons for this:\nA: Another dev-pm instance is already running. \nB: dev-pm crashed and left the file behind. In this case please remove the file manually."); + return; + } } catch (e) { } const server = createServer(); From a9f8b6fdc3664b41e12895d43eaf97ed5bd54cfa Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Fri, 1 Apr 2022 13:09:32 +0200 Subject: [PATCH 05/10] Docs: Use dev-pm.config.js in readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 59bcc32..0c9e29d 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ $ npm run build ## Usage -Add `pm.config.js` file to the project root. +Add `dev-pm.config.js` file to the project root. This file defines all available apps, which should be started by dev-process-manager. -### pm.config.js +### dev-pm.config.js ```javascript module.exports = { @@ -33,7 +33,7 @@ Update dev script in package.json ```json ... - "dev": "dotenv -- node /lib/index.js start pm.config.js", + "dev": "dotenv -- node /lib/index.js start dev-pm.config.js", ... ``` @@ -43,7 +43,7 @@ Update dev script in package.json Either use the package.json script and run `npm run dev` or start with: ```console -$ node /lib/index.js start +$ node /lib/index.js start ``` ### Shutdown From 11055a0abffeb2f6f7deb66af7827fbcf4dda2cd Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Fri, 1 Apr 2022 13:28:27 +0200 Subject: [PATCH 06/10] Docs: Simplify examples --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0c9e29d..955ba6d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ module.exports = { apps: [ { name: "api", - script: "npm --prefix api install && dotenv -- wait-on -l tcp:$POSTGRESQL_PORT && npm run --prefix api fixtures:dev && npm --prefix api run start:dev", + script: "npm run start", }, ... ], @@ -33,14 +33,14 @@ Update dev script in package.json ```json ... - "dev": "dotenv -- node /lib/index.js start dev-pm.config.js", + "start": "node /lib/index.js start dev-pm.config.js", ... ``` ## Commands ### Start -Either use the package.json script and run `npm run dev` +Either use the package.json script and run `npm run start` or start with: ```console $ node /lib/index.js start From f10814d655875e7d370b18d5b4e8a95f61e32d20 Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Fri, 8 Apr 2022 13:11:23 +0200 Subject: [PATCH 07/10] Refactor: Remove empty try-catch --- src/commands/start.command.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index e462d11..72740b4 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -42,12 +42,10 @@ export const start = async (pmConfigFilePath: string) => { processes[app.name] = p; } - try { - if (existsSync("./.pm.sock")) { - console.log("Could not start dev-pm server. A '.pm.sock' file already exists. \nThere are 2 possible reasons for this:\nA: Another dev-pm instance is already running. \nB: dev-pm crashed and left the file behind. In this case please remove the file manually."); - return; - } - } catch (e) { } + if (existsSync("./.pm.sock")) { + console.log("Could not start dev-pm server. A '.pm.sock' file already exists. \nThere are 2 possible reasons for this:\nA: Another dev-pm instance is already running. \nB: dev-pm crashed and left the file behind. In this case please remove the file manually."); + return; + } const server = createServer(); server.listen(".pm.sock"); From 02516a5685fac6c245219c3d9bb8a87cb0da01cc Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Thu, 14 Apr 2022 16:37:13 +0200 Subject: [PATCH 08/10] Refactor: Simplify kill processes --- src/commands/start.command.ts | 75 +++-------------------------------- 1 file changed, 5 insertions(+), 70 deletions(-) diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index 72740b4..bf8114a 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -11,7 +11,7 @@ export const start = async (pmConfigFilePath: string) => { function startProcess(app: AppDefinition) { console.log("starting " + app.script); - const p = spawn("bash", ["-c", app.script]); + const p = spawn("bash", ["-c", app.script], { detached: true }); p.stdout.on('data', data => { process.stdout.write(data); logSockets.forEach(s => { @@ -30,6 +30,7 @@ export const start = async (pmConfigFilePath: string) => { }); p.on('close', code => { if (!shuttingDown) { + console.log('DEBUG code:', code); console.error("process stopped", app.name, ", restarting"); startProcess(app); } @@ -100,7 +101,7 @@ export const start = async (pmConfigFilePath: string) => { running: !p.killed } }); - console.log("sending status reponse", response); + console.log("sending status response", response); s.write(JSON.stringify(response)); s.end(); } else if (cmd == "shutdown") { @@ -123,7 +124,7 @@ export const start = async (pmConfigFilePath: string) => { shutdown(); }); - const events = ["beforeExit", "disconnected", "message", "rejectionHandled", "uncaughtException", "exit", "SIGABRT", "SIGHUP", "SIGPWR", "SIGQUIT"]; + const events = ["beforeExit", "disconnected", "message", "rejectionHandled", "uncaughtException", "SIGABRT", "SIGHUP", "SIGPWR", "SIGQUIT"]; events.forEach((eventName) => { process.on(eventName, (...args) => { @@ -138,77 +139,11 @@ export const start = async (pmConfigFilePath: string) => { shuttingDown = true; await Promise.all(Object.values(processes).map(async p => { if (p.pid) { - const list: { [key: string]: string[] } = await getChildProcesses(p.pid.toString(), { [p.pid]: [] }, { [p.pid]: 1 }); - killProcesses(list); + process.kill(-p.pid); } })); server.close(); s?.destroy(); process.exit(); } - - const killProcesses = (tree: { [key: string]: string[] }) => { - const killed: { [key: string]: number } = {}; - Object.keys(tree).map((pid) => { - tree[pid].map((childPid) => { - if (!killed[childPid]) { - killPid(childPid); - killed[childPid] = 1; - } - if (!killed[pid]) { - killPid(pid); - killed[pid] = 1; - } - }) - }) - return; - } - - const killPid = (pid: string) => { - try { - process.kill(parseInt(pid, 10), "SIGINT"); - } catch (err) { - console.error(err); - } - } - - const getChildProcesses = async (parentPid: string, tree: { [key: string]: string[] }, pidsToProcess: { [key: string]: number }): Promise<{ [key: string]: string[] }> => { - return new Promise((resolve, reject) => { - const ps = spawn('pgrep', ['-P', parentPid]); - let allData = ""; - - const onClose = (code: unknown) => { - delete pidsToProcess[parentPid]; - if (code != 0) { - if (Object.keys(pidsToProcess).length === 0) { - resolve(tree); - } - return tree; - } - - const pids = allData.match(/\d+/g) || []; - if (pids.length === 0) { - return resolve(tree); - } - pids.forEach(function (pid) { - tree[parentPid].push(pid); - tree[pid] = []; - pidsToProcess[pid] = 1; - resolve(getChildProcesses(pid, tree, pidsToProcess)); - }) - }; - - ps.on('error', function (err) { - console.error(err); - reject(err); - }); - - ps.stdout.on('data', function (data) { - data = data.toString('ascii'); - allData += data; - }) - - ps.on('close', onClose); - }) - } } From 13a737ee3d1115784446814df3dc065d696b76a0 Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Thu, 14 Apr 2022 16:47:43 +0200 Subject: [PATCH 09/10] Chore: Remove not needed console.log --- src/commands/start.command.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index bf8114a..5e21c21 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -28,9 +28,8 @@ export const start = async (pmConfigFilePath: string) => { } }); }); - p.on('close', code => { + p.on('close', () => { if (!shuttingDown) { - console.log('DEBUG code:', code); console.error("process stopped", app.name, ", restarting"); startProcess(app); } From feae390d52b94fd64cb4f42f7ec08f3133d27a93 Mon Sep 17 00:00:00 2001 From: Fabian Hoffmann Date: Wed, 20 Apr 2022 09:37:11 +0200 Subject: [PATCH 10/10] Feat: Make path to config file optional --- README.md | 7 +++++-- src/commands/start.command.ts | 3 ++- src/index.ts | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 955ba6d..8d01329 100644 --- a/README.md +++ b/README.md @@ -29,21 +29,24 @@ module.exports = { Update dev script in package.json + ### package.json ```json ... - "start": "node /lib/index.js start dev-pm.config.js", + "start": "node /lib/index.js start", ... ``` +The path to the config file can be specified in an optional parameter. "dev-pm.config.js" in the root directory is used by default. + ## Commands ### Start Either use the package.json script and run `npm run start` or start with: ```console -$ node /lib/index.js start +$ node /lib/index.js start [path-to-dev-pm.config.js] ``` ### Shutdown diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index 5e21c21..9c9842f 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -3,7 +3,8 @@ import { Socket, createServer, createConnection } from "net"; import { AppDefinition } from "../app-definition.type"; import { existsSync } from "fs"; -export const start = async (pmConfigFilePath: string) => { +export const start = async (pmConfigFilePathOverride?: string) => { + const pmConfigFilePath = pmConfigFilePathOverride ? pmConfigFilePathOverride : "dev-pm.config.js" const { apps }: { apps: AppDefinition[] } = await import(`${process.cwd()}/${pmConfigFilePath}`); const processes: { [key: string]: ChildProcess } = {}; const logSockets: { socket: Socket, name: string | null; }[] = []; diff --git a/src/index.ts b/src/index.ts index 6044509..7a7ed0a 100755 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { Command } from 'commander'; import { start, shutdown, status, logs, restart } from "./commands"; const program = new Command(); -program.command("start ") +program.command("start [pmConfigFilePath]") .action((pmConfigFilePath) => { start(pmConfigFilePath); });