From d3e22903c5d3d02058e79328ff1648801c4265ed Mon Sep 17 00:00:00 2001 From: Sylvain Cau <10268458+sylc@users.noreply.github.com> Date: Sun, 1 Jan 2023 23:38:02 +0800 Subject: [PATCH] feat: Enable macos (#11) --- .github/workflows/ci.yml | 10 +++-- .vscode/settings.json | 3 +- README.md | 3 +- cli.test.ts | 75 +++++++++++++++++++++++--------------- cli.ts | 6 ++- deno.jsonc | 1 + deps_test.ts | 1 + docs/development.md | 2 +- src/dkill.ts | 1 + src/portToPid.ts | 58 +++++++---------------------- src/procList.ts | 5 ++- src/tests/utils.ts | 10 +++-- src/utils/grepPort.test.ts | 15 ++++++++ src/utils/grepPort.ts | 72 ++++++++++++++++++++++++++++++++++++ 14 files changed, 177 insertions(+), 85 deletions(-) create mode 100644 src/utils/grepPort.test.ts create mode 100644 src/utils/grepPort.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 522e2e8..f5f2bfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: GitHub Actions Demo +name: CI on: [push] jobs: dkill-test: @@ -6,7 +6,11 @@ jobs: timeout-minutes: 3 strategy: matrix: - os: [ubuntu-latest, windows-latest] + os: [ + ubuntu-latest, + windows-latest, + macos-latest + ] steps: - uses: denoland/setup-deno@v1 @@ -15,7 +19,7 @@ jobs: - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" - name: Check out repository code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: lint run: deno lint diff --git a/.vscode/settings.json b/.vscode/settings.json index 1ad8e87..57f3b1d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "deno.lint": true, "deno.suggest.imports.hosts": { "https://deno.land": true - } + }, + "editor.formatOnSave": true } diff --git a/README.md b/README.md index 8fad762..09eece6 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ See [docs](https://doc.deno.land/https://deno.land/x/dkill/mod.ts) - Windows: Windows 8 or above - Linux: On linux the cmd `ss` is used, which works on ubuntu 16.04 and above. -- Mac: Not implemented. PR welcome. +- MacOS: The command `lsof` is used. Interactive mode and listing the exact + command is currently not implemented ## Inspiration diff --git a/cli.test.ts b/cli.test.ts index 4a5eb6c..91c619b 100644 --- a/cli.test.ts +++ b/cli.test.ts @@ -1,4 +1,4 @@ -import { assertNotEquals, delay } from "./deps_test.ts"; +import { assertEquals, assertNotEquals, delay } from "./deps_test.ts"; Deno.test("killing by pid", async () => { // create pid @@ -11,7 +11,7 @@ Deno.test("killing by pid", async () => { cmd: ["deno", "run", "-A", "./cli.ts", `${pTest.pid}`], }); // wait dkill finishes - await pDkill.status(); + const cliStatus = await pDkill.status(); // retreive status from test pid const status = await pTest.status(); @@ -20,39 +20,56 @@ Deno.test("killing by pid", async () => { pTest.close(); pDkill.close(); + // ensure dkill existed cleanly + assertEquals(cliStatus.code, 0); assertNotEquals(status.code, 0); }); -Deno.test("killing by ports", async () => { - // create a webserver - const pTest1 = Deno.run({ - cmd: ["deno", "run", "-A", "./src/tests/utils.ts"], - }); - const pTest2 = Deno.run({ - cmd: ["deno", "run", "-A", "./src/tests/utils.ts", "8081"], - }); - - // give time fo the webserver to start and the port be discoverable - await delay(5000); - - // call dkill - const pDkill = Deno.run({ - cmd: ["deno", "run", "-A", "./cli.ts", ":8080", ":8081"], - }); - // wait dkill finishes - await pDkill.status(); +Deno.test({ + name: "killing by ports", + fn: async () => { + // create a webserver + const pTest1 = Deno.run({ + cmd: ["deno", "run", "-A", "./src/tests/utils.ts"], + }); + const pTest2 = Deno.run({ + cmd: ["deno", "run", "-A", "./src/tests/utils.ts", "8081"], + }); - // retrieve status from test pid - const status1 = await pTest1.status(); - const status2 = await pTest2.status(); + // give time fo the webserver to start and the port be discoverable + await delay(5000); - // close resources - pTest1.close(); - pTest2.close(); + // call dkill + const pDkill = Deno.run({ + cmd: [ + "deno", + "run", + "-A", + "--unstable", + "./cli.ts", + "--verbose", + ":8080", + ":8081", + ], + }); + // wait dkill finishes + const cliStatus = await pDkill.status(); + pDkill.close(); + // ensure dkill exited cleanly + assertEquals(cliStatus.code, 0); - pDkill.close(); + // throw Error('xxx') - assertNotEquals(status1.code, 0); - assertNotEquals(status2.code, 0); + // retrieve status from test pid + const status1 = await pTest1.status(); + const status2 = await pTest2.status(); + // close resources + pTest1.close(); + pTest2.close(); + assertNotEquals(status1.code, 0); + assertNotEquals(status1.code, 5); // check it wasn't a timeout + assertNotEquals(status2.code, 0); + assertNotEquals(status2.code, 5); + }, }); diff --git a/cli.ts b/cli.ts index 2f1e5e0..e2db9e5 100644 --- a/cli.ts +++ b/cli.ts @@ -25,7 +25,7 @@ await new Command() You can specify multiple targets at once: 'dkill node.exe :5000 :3000 164'`, ) .arguments("<...targets>") - .option("-i, --interactive", "Interactive mode", { + .option("-i, --interactive", "Interactive mode (Not available on MacOS)", { standalone: true, }) .option("-v, --verbose", "Increase verbosity") @@ -57,6 +57,10 @@ await new Command() const procs: string[] = []; if (opts.interactive) { + if (Deno.build.os === "darwin") { + console.error("Not implemented on macos"); + Deno.exit(1); + } // list processes const pList = await procList(); const pickedProcesses: string[] = await Checkbox.prompt({ diff --git a/deno.jsonc b/deno.jsonc index bfa543b..191d66b 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,7 @@ { "tasks": { "test": "deno test --allow-run --allow-net", + "testm": "deno run --allow-run --allow-net ./src/tests/utils.ts", "dev": "deno run --allow-run --allow-net ./cli.ts", "release": "deno run -A https://deno.land/x/release_up@0.5.0/cli.ts --regex \"(?<=@)(.*)(?=\/cli)\" --github --versionFile --changelog" } diff --git a/deps_test.ts b/deps_test.ts index a3b319e..46d6288 100644 --- a/deps_test.ts +++ b/deps_test.ts @@ -1,5 +1,6 @@ export { assert, + assertEquals, assertNotEquals, } from "https://deno.land/std@0.170.0/testing/asserts.ts"; export { delay } from "https://deno.land/std@0.170.0/async/mod.ts"; diff --git a/docs/development.md b/docs/development.md index 303dbd8..78be26d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -4,4 +4,4 @@ To release a new version run the below: -`deno task release` \ No newline at end of file +`deno task release` diff --git a/src/dkill.ts b/src/dkill.ts index 69cf1ac..db87a5d 100644 --- a/src/dkill.ts +++ b/src/dkill.ts @@ -80,6 +80,7 @@ export async function dkill( if (!opts?.dryrun) { const killedPids = KillPids( allPidsToKill.map((pidItem) => pidItem.pid), + { verbose: opts?.verbose }, ); allPidsToKill = allPidsToKill.map((pidItem) => ({ ...pidItem, diff --git a/src/portToPid.ts b/src/portToPid.ts index be22d4a..67d77d9 100644 --- a/src/portToPid.ts +++ b/src/portToPid.ts @@ -1,3 +1,8 @@ +import { + grepPortLinux, + grepPortMacOS, + grepPortWindows, +} from "./utils/grepPort.ts"; import { runCmd } from "./utils/runCmd.ts"; /** @@ -15,21 +20,7 @@ export async function portToPid(port: number): Promise { `netstat -nao | findstr :${port}`, ]); - // outstring example - // TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 28392 - const parsedLines = outString - .split("\n") - .map((line) => line.match(/\S+/g) || []); - - // parsedLines - // [ [ "TCP", "0.0.0.0:3000", "0.0.0.0:0", "LISTENING", "28392" ], [] ] - - const pidColumnsIndex = 4; - - const pids = parsedLines - .filter((arr) => arr.length !== 0) // filter last line - .map((arr) => +arr[pidColumnsIndex]) // extract pids based on columns - .filter((pid) => Number.isInteger(pid) && pid !== 0); // ensure they are numbers. pid 0 can be ignored + const pids = grepPortWindows(outString); return [...new Set(pids)]; // remove duplicates; } else if (os === "linux") { @@ -38,38 +29,15 @@ export async function portToPid(port: number): Promise { // -n: provide local address const outString = await runCmd(["ss", "-lnp"]); - // outstring example - // tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("deno", pid=200, fd=12)) - const parsedLines = outString - .split("\n") - .map((line) => line.match(/\S+/g) || []); + const pids = grepPortLinux(outString, port); - // parsedLines - // [ [ "LISTEN", "0", "128", "0.0.0.0:8080", "0.0.0.0:*", users:(("deno", pid=200, fd=12)) ], [] ] - - const portColumnIndex = 4; - const pidColumnsIndex = 6; - - // remove first row of titles - parsedLines.shift(); + return [...new Set(pids)]; // remove duplicates; + } else if (os === "darwin") { + const outString = await runCmd(["lsof", "-nwP", `-iTCP:${port}`]); - const pids = parsedLines - .filter((arr) => arr.length !== 0) // filter last line - .filter((arr) => { - const localAddrArr = arr[portColumnIndex].split(":"); - return localAddrArr.length > 0 ? +localAddrArr[1] === port : false; - }) // filter connection for the targetted port - .map((arr) => { - // arr[pidColumnsIndex] should be like: - // users:(("deno", pid=200, fd=12)) - const strArr = arr[pidColumnsIndex].match(/pid=(.*?),/); - if (!strArr) { - console.log("Line with issues", arr); - throw Error("Invalid parsing"); - } - return +strArr[1]; - }) // extract pids based on columns - .filter((pid) => Number.isInteger(pid) && pid !== 0); // ensure they are numbers. pid 0 can be ignored + // COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME + // deno 1407 runner 13u IPv4 0x85fdd397dc6713cf 0t0 TCP *:8081 (LISTEN) + const pids = grepPortMacOS(outString); return [...new Set(pids)]; // remove duplicates; } else { diff --git a/src/procList.ts b/src/procList.ts index 599878c..90db99a 100644 --- a/src/procList.ts +++ b/src/procList.ts @@ -2,7 +2,8 @@ import { runCmd } from "./utils/runCmd.ts"; import { PidItem } from "./utils/types.ts"; /** - * list all process running + * list all process running. + * WARNING. DO NOT USE FOR macos (It returns nothing) * @returns {Promise} Array of Pid infos */ export async function procList(): Promise { @@ -80,6 +81,8 @@ export async function procList(): Promise { .forEach((item) => { resultsObject[item.pid] = { ...resultsObject[item.pid], cmd: item.cmd }; }); + } else if (os === "darwin") { + // TODO: skipped for now } else { throw Error("Platform not supported yet"); } diff --git a/src/tests/utils.ts b/src/tests/utils.ts index 26c238a..91ce50d 100644 --- a/src/tests/utils.ts +++ b/src/tests/utils.ts @@ -1,10 +1,10 @@ /** * webserver.ts */ -import { serve } from "../../deps_test.ts"; +import { delay, serve } from "../../deps_test.ts"; -const rawPort : string | number = Deno.args[0] ?? 8080; -const port = Number(rawPort) +const rawPort: string | number = Deno.args[0] ?? 8080; +const port = Number(rawPort); const handler = (request: Request): Response => { let body = "Your user-agent is:\n\n"; @@ -14,4 +14,8 @@ const handler = (request: Request): Response => { }; console.log(`HTTP webserver running. Access it at: http://localhost:${port}/`); +delay(25000).then(() => { + Deno.exit(5); +}); + await serve(handler, { port }); diff --git a/src/utils/grepPort.test.ts b/src/utils/grepPort.test.ts new file mode 100644 index 0000000..042c3b3 --- /dev/null +++ b/src/utils/grepPort.test.ts @@ -0,0 +1,15 @@ +import { assertEquals } from "../../deps_test.ts"; +import { grepPortMacOS, grepPortWindows } from "./grepPort.ts"; + +Deno.test("assertMinVersion", () => { + // windows + const outw = + "TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 28392"; + assertEquals(grepPortWindows(outw), [28392]); + + // macOS + const outmac = + `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME + deno 1407 runner 13u IPv4 0x85fdd397dc6713cf 0t0 TCP *:8081 (LISTEN)`; + assertEquals(grepPortMacOS(outmac), [1407]); +}); diff --git a/src/utils/grepPort.ts b/src/utils/grepPort.ts new file mode 100644 index 0000000..1251202 --- /dev/null +++ b/src/utils/grepPort.ts @@ -0,0 +1,72 @@ +export const grepPortWindows = (rawLog: string) => { + // outstring example + // TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 28392 + const parsedLines = rawLog + .split("\n") + .map((line) => line.match(/\S+/g) || []); + + // parsedLines + // [ [ "TCP", "0.0.0.0:3000", "0.0.0.0:0", "LISTENING", "28392" ], [] ] + + const pidColumnsIndex = 4; + + const pids = parsedLines + .filter((arr) => arr.length !== 0) // filter last line + .map((arr) => +arr[pidColumnsIndex]) // extract pids based on columns + .filter((pid) => Number.isInteger(pid) && pid !== 0); // ensure they are numbers. pid 0 can be ignored + return pids; +}; + +export const grepPortLinux = (rawLog: string, port: number) => { + // outstring example + // tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("deno", pid=200, fd=12)) + const parsedLines = rawLog + .split("\n") + .map((line) => line.match(/\S+/g) || []); + + // parsedLines + // [ [ "LISTEN", "0", "128", "0.0.0.0:8080", "0.0.0.0:*", users:(("deno", pid=200, fd=12)) ], [] ] + + const portColumnIndex = 4; + const pidColumnsIndex = 6; + + // remove first row of titles + parsedLines.shift(); + + const pids = parsedLines + .filter((arr) => arr.length !== 0) // filter last line + .filter((arr) => { + const localAddrArr = arr[portColumnIndex].split(":"); + return localAddrArr.length > 0 ? +localAddrArr[1] === port : false; + }) // filter connection for the targetted port + .map((arr) => { + // arr[pidColumnsIndex] should be like: + // users:(("deno", pid=200, fd=12)) + const strArr = arr[pidColumnsIndex].match(/pid=(.*?),/); + if (!strArr) { + console.log("Line with issues", arr); + throw Error("Invalid parsing"); + } + return +strArr[1]; + }) // extract pids based on columns + .filter((pid) => Number.isInteger(pid) && pid !== 0); // ensure they are numbers. pid 0 can be ignored + + return pids; +}; + +export const grepPortMacOS = (rawLog: string) => { + const parsedLines = rawLog.split("\n") + .map((line) => line.match(/\S+/g) || []); + + const pidColumnsIndex = 1; + + // remove the headers + parsedLines.shift(); + + const pids = parsedLines + .filter((arr) => arr.length !== 0) // filter invalid arrays + .map((arr) => +arr[pidColumnsIndex]) // extract pids based on columns + .filter((pid) => Number.isInteger(pid) && pid !== 0); // ensure they are numbers. pid 0 can be ignored + + return pids; +};