From e72d21e03aec4edb61a4a581dac98e21e69c0c2f Mon Sep 17 00:00:00 2001 From: Yanis Benson Date: Thu, 8 Jul 2021 17:54:39 +0300 Subject: [PATCH] Add `--force-after-timeout` / `-t` option --- cli.js | 14 ++++++--- interactive.js | 83 +++++++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- readme.md | 9 +++--- 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/cli.js b/cli.js index c2827ce..e8b6ba4 100755 --- a/cli.js +++ b/cli.js @@ -8,9 +8,10 @@ const cli = meow(` $ fkill [ …] Options - --force -f Force kill - --verbose -v Show process arguments - --silent -s Silently kill and always exit with code 0 + --force -f Force kill + --verbose -v Show process arguments + --silent -s Silently kill and always exit with code 0 + --force-after-timeout , -t Force kill processes which didn't exit after N seconds Examples $ fkill 1337 @@ -39,6 +40,10 @@ const cli = meow(` silent: { type: 'boolean', alias: 's' + }, + forceAfterTimeout: { + type: 'number', + alias: 't' } } }); @@ -46,7 +51,8 @@ const cli = meow(` if (cli.input.length === 0) { require('./interactive').init(cli.flags); } else { - const promise = fkill(cli.input, {...cli.flags, ignoreCase: true}); + const forceAfterTimeout = cli.flags.forceAfterTimeout === undefined ? undefined : cli.flags.forceAfterTimeout * 1000; + const promise = fkill(cli.input, {...cli.flags, forceAfterTimeout, ignoreCase: true}); if (!cli.flags.force) { promise.catch(error => { diff --git a/interactive.js b/interactive.js index 83ccf7f..756a0ec 100644 --- a/interactive.js +++ b/interactive.js @@ -7,10 +7,41 @@ const escExit = require('esc-exit'); const cliTruncate = require('cli-truncate'); const pidFromPort = require('pid-port'); const fkill = require('fkill'); +const processExists = require('process-exists'); const isWindows = process.platform === 'win32'; const commandLineMargins = 4; +const PROCESS_EXITED_MIN_INTERVAL = 5; +const PROCESS_EXITED_MAX_INTERVAL = 1280; + +const delay = ms => new Promise(resolve => { + setTimeout(resolve, ms); +}); + +const processExited = async (pid, timeout) => { + const endTime = Date.now() + timeout; + let interval = PROCESS_EXITED_MIN_INTERVAL; + if (interval > timeout) { + interval = timeout; + } + + let exists; + + do { + await delay(interval); // eslint-disable-line no-await-in-loop + + exists = await processExists(pid); // eslint-disable-line no-await-in-loop + + interval *= 2; + if (interval > PROCESS_EXITED_MAX_INTERVAL) { + interval = PROCESS_EXITED_MAX_INTERVAL; + } + } while (Date.now() < endTime && exists); + + return !exists; +}; + const nameFilter = (input, process_) => { const isPort = input[0] === ':'; @@ -131,6 +162,52 @@ const handleFkillError = async processes => { } }; +const DEFAULT_EXIT_TIMEOUT = 3000; + +const performKillSequence = async processes => { + if (!Array.isArray(processes)) { + processes = [processes]; + } + + let didSurvive; + let hadError; + try { + await fkill(processes); + const exited = await Promise.all(processes.map(process => processExited(process, DEFAULT_EXIT_TIMEOUT))); + didSurvive = processes.filter((_, i) => !exited[i]); + } catch (error) { + didSurvive = processes; + hadError = error; + } + + if (didSurvive.length === 0) { + return; + } + + const suffix = didSurvive.length > 1 ? 'es' : ''; + const problemText = hadError ? `Error killing process${suffix}.` : `Process${suffix} didn't exit in ${DEFAULT_EXIT_TIMEOUT}ms.`; + + if (process.stdout.isTTY === false) { + console.error(`${problemText} Try \`fkill --force ${didSurvive.join(' ')}\``); + process.exit(1); // eslint-disable-line unicorn/no-process-exit + } + + const answer = await inquirer.prompt([{ + type: 'confirm', + name: 'forceKill', + message: `${problemText} Would you like to use the force?` + }]); + + if (!answer.forceKill) { + return; + } + + await fkill(processes, { + force: true, + ignoreCase: true + }); +}; + const listProcesses = async (processes, flags) => { inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); @@ -142,11 +219,7 @@ const listProcesses = async (processes, flags) => { source: async (answers, input) => filterProcesses(input, processes, flags) }]); - try { - await fkill(answer.processes); - } catch { - handleFkillError(answer.processes); - } + performKillSequence(answer.processes); }; const init = async flags => { diff --git a/package.json b/package.json index 6b05e5a..ec25b54 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "chalk": "^4.1.0", "cli-truncate": "^2.1.0", "esc-exit": "^2.0.2", - "fkill": "^7.1.0", + "fkill": "^7.2.1", "inquirer": "^7.3.3", "inquirer-autocomplete-prompt": "^1.3.0", "meow": "^8.1.0", diff --git a/readme.md b/readme.md index e3d848e..1c6e5ad 100644 --- a/readme.md +++ b/readme.md @@ -52,12 +52,13 @@ $ npm install --global fkill-cli $ fkill --help Usage - $ fkill [ …] + $ fkill [ …] Options - --force -f Force kill - --verbose -v Show process arguments - --silent -s Silently kill and always exit with code 0 + --force, -f Force kill + --verbose, -v Show process arguments + --silent, -s Silently kill and always exit with code 0 + --force-timeout , -t Force kill processes which didn't exit after N seconds Examples $ fkill 1337