diff --git a/index.js b/index.js index f9453d1..efd2778 100644 --- a/index.js +++ b/index.js @@ -262,9 +262,8 @@ export default async function fkill(inputs, options = {}) { } }))); - const exists = await processExistsMultiple([...parsedInputsMap.values()]); - const errors = []; + const killFailures = []; const handleKill = async input => { const parsedInput = parsedInputsMap.get(input); @@ -272,16 +271,24 @@ export default async function fkill(inputs, options = {}) { try { await killWithLimits(input, options); } catch (error) { + killFailures.push({input, parsedInput, error}); + } + }; + + await Promise.all(inputs.map(input => handleKill(input))); + + if (killFailures.length > 0 && !options.silent) { + const exists = await processExistsMultiple(killFailures.map(({parsedInput}) => parsedInput)); + + for (const {input, parsedInput, error} of killFailures) { if (!exists.get(parsedInput)) { errors.push(`Killing process ${input} failed: Process doesn't exist`); - return; + continue; } errors.push(`Killing process ${input} failed: ${error.message.replace(/.*\n/, '').replace(/kill: \d+: /, '').trim()}`); } - }; - - await Promise.all(inputs.map(input => handleKill(input))); + } if (errors.length > 0 && !options.silent) { throw new AggregateError(errors, 'Failed to kill processes'); diff --git a/lazy-existence-check.test.js b/lazy-existence-check.test.js new file mode 100644 index 0000000..41edef2 --- /dev/null +++ b/lazy-existence-check.test.js @@ -0,0 +1,105 @@ +import {test} from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; + +const createDataModule = source => `data:text/javascript,${encodeURIComponent(source)}`; + +const loadFkillWithMocks = async ({killError = false, processExistsResult = true} = {}) => { + const callsKey = `__fkillMockCalls_${Date.now()}_${Math.random().toString(36).slice(2)}`; + const calls = { + events: [], + execa: [], + processExistsMultiple: [], + taskkill: [], + }; + + globalThis[callsKey] = calls; + + const getCalls = `const calls = globalThis[${JSON.stringify(callsKey)}];`; + const killImplementation = killError + ? 'throw Object.assign(new Error(\'kill failed\'), {exitCode: 1});' + : 'return undefined;'; + + const taskkillModule = createDataModule(` + ${getCalls} + export const taskkill = async (...arguments_) => { + calls.events.push('kill'); + calls.taskkill.push(arguments_); + ${killImplementation} + }; + `); + + const execaModule = createDataModule(` + ${getCalls} + export const execa = async (...arguments_) => { + calls.events.push('kill'); + calls.execa.push(arguments_); + ${killImplementation} + }; + `); + + const processExistsModule = createDataModule(` + ${getCalls} + export const processExistsMultiple = async processes => { + calls.events.push('exists'); + calls.processExistsMultiple.push(processes); + return new Map(processes.map(process_ => [process_, ${JSON.stringify(processExistsResult)}])); + }; + + export const filterExistingProcesses = async processes => processes; + `); + + const indexSource = await readFile(new URL('index.js', import.meta.url), 'utf8'); + const source = indexSource + .replace('from \'taskkill\';', `from ${JSON.stringify(taskkillModule)};`) + .replace('from \'execa\';', `from ${JSON.stringify(execaModule)};`) + .replace('from \'pid-port\';', `from ${JSON.stringify(createDataModule('export const portToPid = async () => 1234;'))};`) + .replace('from \'process-exists\';', `from ${JSON.stringify(processExistsModule)};`) + .replace('from \'ps-list\';', `from ${JSON.stringify(createDataModule('export default async () => [];'))};`); + + const {default: fkill} = await import(createDataModule(source)); + + return { + fkill, + calls, + cleanup() { + delete globalThis[callsKey]; + }, + }; +}; + +test('does not check process existence before a successful kill', async () => { + const {fkill, calls, cleanup} = await loadFkillWithMocks(); + + try { + await fkill(1234, {force: true}); + + assert.deepEqual(calls.events, ['kill']); + assert.equal(calls.processExistsMultiple.length, 0); + } finally { + cleanup(); + } +}); + +test('checks process existence only after a failed kill', async () => { + const {fkill, calls, cleanup} = await loadFkillWithMocks({ + killError: true, + processExistsResult: false, + }); + + try { + await assert.rejects( + fkill(1234, {force: true}), + error => { + assert.ok(error instanceof AggregateError); + assert.match(error.errors.join(' '), /Process doesn't exist/); + return true; + }, + ); + + assert.deepEqual(calls.events, ['kill', 'exists']); + assert.equal(calls.processExistsMultiple.length, 1); + } finally { + cleanup(); + } +});