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
73 changes: 70 additions & 3 deletions interactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import FuzzySearch from 'fuzzy-search';

const isWindows = process.platform === 'win32';
const commandLineMargins = 4;
const processGroupSymbol = Symbol('processGroup');

const PROCESS_EXITED_MIN_INTERVAL = 5;
const PROCESS_EXITED_MAX_INTERVAL = 1280;
Expand Down Expand Up @@ -117,6 +118,42 @@ const renderProcessForDisplay = (process_, flags, memoryThreshold, cpuThreshold)
};
};

const renderProcessGroupForDisplay = (processGroup, flags, memoryThreshold, cpuThreshold) => {
const mostExpensiveProcess = [...processGroup.processes].sort(preferHighPerformanceImpact)[0];
const displayProcess = {
...mostExpensiveProcess,
name: `${processGroup.name} (${processGroup.processes.length} processes)`,
cmd: `${processGroup.name} (${processGroup.processes.length} processes)`,
pid: processGroup.processes[0].pid,
ports: processGroup.processes.flatMap(process_ => process_.ports),
};
const renderedProcess = renderProcessForDisplay(displayProcess, flags, memoryThreshold, cpuThreshold);

return {
...renderedProcess,
value: processGroup,
};
};

const groupProcessesByName = processes => {
const processGroups = new Map();

for (const process_ of processes) {
const processGroup = processGroups.get(process_.name) ?? {
[processGroupSymbol]: true,
name: process_.name,
processes: [],
};

processGroup.processes.push(process_);
processGroups.set(process_.name, processGroup);
}

return [...processGroups.values()].flatMap(processGroup => processGroup.processes.length === 1 ? processGroup.processes : processGroup);
};

const isProcessGroup = value => value?.[processGroupSymbol] === true;

const searchProcessesByPort = (processes, port) => processes.filter(process_ => process_.ports.includes(port));

const searchProcessByPid = (processes, pid) => processes.find(process_ => String(process_.pid) === pid);
Expand Down Expand Up @@ -175,6 +212,10 @@ const filterAndSortProcesses = (processes, term, searcher, flags) => {
return searchProcessesByName(filtered, term, searcher, flags);
};

const renderProcessOrGroupForDisplay = (processOrGroup, flags, memoryThreshold, cpuThreshold) => isProcessGroup(processOrGroup)
? renderProcessGroupForDisplay(processOrGroup, flags, memoryThreshold, cpuThreshold)
: renderProcessForDisplay(processOrGroup, flags, memoryThreshold, cpuThreshold);

const handleFkillError = async (inputs, flags = {}) => {
const shouldForceKill = await promptForceKill(inputs, 'Error killing process.');

Expand Down Expand Up @@ -239,6 +280,31 @@ const performKillSequence = async processes => {
}
};

const selectProcessesFromGroup = async processGroup => {
const answer = await inquirer.prompt([{
type: 'list',
name: 'selection',
message: `Kill ${processGroup.name}:`,
choices: [
{
name: `All ${processGroup.processes.length} processes`,
value: processGroup.processes.map(process_ => process_.pid),
},
...processGroup.processes.map(process_ => ({
name: `${process_.name} ${chalk.dim(process_.pid)}`,
value: process_.pid,
})),
],
}]);

return answer.selection;
};

const performSelectedKillSequence = async selectedValue => {
const processSelection = isProcessGroup(selectedValue) ? await selectProcessesFromGroup(selectedValue) : selectedValue;
await performKillSequence(processSelection);
};

const findPortsForProcess = (processId, portToPidMap) => {
const ports = [];

Expand All @@ -261,11 +327,12 @@ const listProcesses = async (processes, flags) => {
pageSize: 10,
async source(term = '') {
const matchingProcesses = filterAndSortProcesses(processes, term, searcher, flags);
return matchingProcesses.map(process_ => renderProcessForDisplay(process_, flags, memoryThreshold, cpuThreshold));
return groupProcessesByName(matchingProcesses)
.map(processOrGroup => renderProcessOrGroupForDisplay(processOrGroup, flags, memoryThreshold, cpuThreshold));
},
});

performKillSequence(selectedPid);
await performSelectedKillSequence(selectedPid);
};

const init = async flags => {
Expand All @@ -284,4 +351,4 @@ const init = async flags => {
listProcesses(processesWithPorts, flags);
};

export {init, handleFkillError};
export {init, handleFkillError, groupProcessesByName};
15 changes: 15 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import delay from 'delay';
import noopProcess from 'noop-process';
import {processExists} from 'process-exists';
import getPort from 'get-port';
import {groupProcessesByName} from './interactive.js';

const noopProcessKilled = async (t, pid) => {
// Ensure the noop process has time to exit
Expand Down Expand Up @@ -57,6 +58,20 @@ test('silently force killing process at unused port exits with code 0', async t
t.is(exitCode, 0);
});

test('groups processes with the same name', t => {
const processes = [
{name: 'node', pid: 1},
{name: 'node', pid: 2},
{name: 'zsh', pid: 3},
];
const result = groupProcessesByName(processes);

t.is(result.length, 2);
t.is(result[0].name, 'node');
t.deepEqual(result[0].processes, [processes[0], processes[1]]);
t.is(result[1], processes[2]);
});

// Case-sensitivity tests only work on Unix-like systems
// Windows process names work differently and don't support custom titles via noopProcess
if (process.platform !== 'win32') {
Expand Down