Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,26 +262,33 @@ 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);

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');
Expand Down
105 changes: 105 additions & 0 deletions lazy-existence-check.test.js
Original file line number Diff line number Diff line change
@@ -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();
}
});