|
1 | 1 | import process from 'node:process'; |
2 | 2 | import chalk from 'chalk'; |
3 | 3 | import inquirer from 'inquirer'; |
4 | | -import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt'; |
| 4 | +import search from '@inquirer/search'; |
5 | 5 | import psList from 'ps-list'; |
6 | 6 | import {numberSortDescending} from 'num-sort'; |
7 | 7 | import escExit from 'esc-exit'; |
@@ -89,160 +89,189 @@ const preferHeurisicallyInterestingProcesses = (a, b) => { |
89 | 89 | return preferLowAlphanumericNames(a, b); |
90 | 90 | }; |
91 | 91 |
|
92 | | -const filterProcesses = (input, processes, flags) => { |
93 | | - const memoryThreshold = flags.verbose ? 0 : 1; |
94 | | - const cpuThreshold = flags.verbose ? 0 : 3; |
| 92 | +const isHelperProcess = process_ => process_.name.endsWith('-helper') |
| 93 | + || process_.name.endsWith('Helper') |
| 94 | + || process_.name.endsWith('HelperApp'); |
95 | 95 |
|
96 | | - const filteredProcesses = new FuzzySearch( |
97 | | - processes, |
98 | | - [ |
99 | | - // The name is truncated for some reason, so we always use `cmd` for now. |
100 | | - 'cmd', |
101 | | - /// flags.verbose && !isWindows ? 'cmd' : 'name', |
102 | | - 'pid', |
103 | | - ], |
104 | | - { |
105 | | - caseSensitive: false, |
106 | | - }, |
107 | | - ) |
108 | | - .search(input); |
109 | | - |
110 | | - return filteredProcesses |
111 | | - .filter(process_ => !( |
112 | | - process_.name.endsWith('-helper') |
113 | | - || process_.name.endsWith('Helper') |
114 | | - || process_.name.endsWith('HelperApp') |
115 | | - )) |
116 | | - .sort(preferHeurisicallyInterestingProcesses) |
117 | | - .map(process_ => { |
118 | | - const renderPercentage = percents => { |
119 | | - const digits = Math.floor(percents * 10).toString().padStart(2, '0'); |
120 | | - const whole = digits.slice(0, -1); |
121 | | - const fraction = digits.slice(-1); |
122 | | - return fraction === '0' ? `${whole}%` : `${whole}.${fraction}%`; |
123 | | - }; |
124 | | - |
125 | | - const lineLength = process.stdout.columns || 80; |
126 | | - const ports = process_.ports.length === 0 ? '' : (' ' + process_.ports.slice(0, 4).map(x => `:${x}`).join(' ')); |
127 | | - const memory = (process_.memory !== undefined && (process_.memory > memoryThreshold)) ? ` 🐏${renderPercentage(process_.memory)}` : ''; |
128 | | - const cpu = (process_.cpu !== undefined && (process_.cpu > cpuThreshold)) ? `🚦${renderPercentage(process_.cpu)}` : ''; |
129 | | - const margins = commandLineMargins + process_.pid.toString().length + ports.length + memory.length + cpu.length; |
130 | | - const length = lineLength - margins; |
131 | | - const name = cliTruncate(flags.verbose && !isWindows ? process_.cmd : process_.name, length, {position: 'middle', preferTruncationOnSpace: true}); |
132 | | - const extraMargin = 2; |
133 | | - const spacer = lineLength === process.stdout.columns ? ''.padEnd(length - name.length - extraMargin) : ''; |
134 | | - |
135 | | - return { |
136 | | - name: `${name} ${chalk.dim(process_.pid)}${spacer}${chalk.dim(ports)}${cpu}${memory}`, |
137 | | - value: process_.pid, |
138 | | - }; |
139 | | - }); |
| 96 | +const renderPercentage = percents => { |
| 97 | + const digits = Math.floor(percents * 10).toString().padStart(2, '0'); |
| 98 | + const whole = digits.slice(0, -1); |
| 99 | + const fraction = digits.slice(-1); |
| 100 | + return fraction === '0' ? `${whole}%` : `${whole}.${fraction}%`; |
140 | 101 | }; |
141 | 102 |
|
142 | | -const handleFkillError = async processes => { |
143 | | - const suffix = processes.length > 1 ? 'es' : ''; |
| 103 | +const renderProcessForDisplay = (process_, flags, memoryThreshold, cpuThreshold) => { |
| 104 | + const lineLength = process.stdout.columns || 80; |
| 105 | + const ports = process_.ports.length === 0 ? '' : (' ' + process_.ports.slice(0, 4).map(x => `:${x}`).join(' ')); |
| 106 | + const memory = (process_.memory !== undefined && (process_.memory > memoryThreshold)) ? ` 🐏${renderPercentage(process_.memory)}` : ''; |
| 107 | + const cpu = (process_.cpu !== undefined && (process_.cpu > cpuThreshold)) ? `🚦${renderPercentage(process_.cpu)}` : ''; |
| 108 | + const margins = commandLineMargins + process_.pid.toString().length + ports.length + memory.length + cpu.length; |
| 109 | + const length = lineLength - margins; |
| 110 | + const name = cliTruncate(flags.verbose && !isWindows ? process_.cmd : process_.name, length, {position: 'middle', preferTruncationOnSpace: true}); |
| 111 | + const extraMargin = 2; |
| 112 | + const spacer = lineLength === process.stdout.columns ? ''.padEnd(length - name.length - extraMargin) : ''; |
| 113 | + |
| 114 | + return { |
| 115 | + name: `${name} ${chalk.dim(process_.pid)}${spacer}${chalk.dim(ports)}${cpu}${memory}`, |
| 116 | + value: process_.pid, |
| 117 | + }; |
| 118 | +}; |
144 | 119 |
|
145 | | - if (process.stdout.isTTY === false) { |
146 | | - console.error(`Error killing process${suffix}. Try \`fkill --force ${processes.join(' ')}\``); |
147 | | - process.exit(1); // eslint-disable-line unicorn/no-process-exit |
148 | | - } else { |
149 | | - const answer = await inquirer.prompt([{ |
150 | | - type: 'confirm', |
151 | | - name: 'forceKill', |
152 | | - message: 'Error killing process. Would you like to use the force?', |
153 | | - }]); |
154 | | - |
155 | | - if (answer.forceKill === true) { |
156 | | - await fkill(processes, { |
157 | | - force: true, |
158 | | - ignoreCase: true, |
159 | | - }); |
| 120 | +const searchProcessesByPort = (processes, port) => processes.filter(process_ => process_.ports.includes(port)); |
| 121 | + |
| 122 | +const searchProcessByPid = (processes, pid) => processes.find(process_ => String(process_.pid) === pid); |
| 123 | + |
| 124 | +const searchProcessesByName = (processes, term, searcher) => { |
| 125 | + const lowerTerm = term.toLowerCase(); |
| 126 | + const exactMatches = []; |
| 127 | + const startsWithMatches = []; |
| 128 | + const containsMatches = []; |
| 129 | + |
| 130 | + for (const process_ of processes) { |
| 131 | + const lowerName = process_.name.toLowerCase(); |
| 132 | + if (lowerName === lowerTerm) { |
| 133 | + exactMatches.push(process_); |
| 134 | + } else if (lowerName.startsWith(lowerTerm)) { |
| 135 | + startsWithMatches.push(process_); |
| 136 | + } else if (lowerName.includes(lowerTerm)) { |
| 137 | + containsMatches.push(process_); |
160 | 138 | } |
161 | 139 | } |
| 140 | + |
| 141 | + // Fuzzy matches (excluding all exact/starts/contains matches) |
| 142 | + const matchedPids = new Set([...exactMatches, ...startsWithMatches, ...containsMatches].map(process_ => process_.pid)); |
| 143 | + const fuzzyResults = searcher.search(term).filter(process_ => !matchedPids.has(process_.pid)); |
| 144 | + |
| 145 | + // Combine in priority order |
| 146 | + return [...exactMatches, ...startsWithMatches, ...containsMatches, ...fuzzyResults]; |
162 | 147 | }; |
163 | 148 |
|
164 | | -const DEFAULT_EXIT_TIMEOUT = 3000; |
| 149 | +const filterAndSortProcesses = (processes, term, searcher) => { |
| 150 | + const filtered = processes.filter(process_ => !isHelperProcess(process_)); |
165 | 151 |
|
166 | | -const performKillSequence = async processes => { |
167 | | - if (!Array.isArray(processes)) { |
168 | | - processes = [processes]; |
| 152 | + // No search term: show all sorted by performance |
| 153 | + if (!term) { |
| 154 | + return filtered.sort(preferHeurisicallyInterestingProcesses); |
169 | 155 | } |
170 | 156 |
|
171 | | - let didSurvive; |
172 | | - let hadError; |
173 | | - try { |
174 | | - await fkill(processes); |
175 | | - const exited = await Promise.all(processes.map(process => processExited(process, DEFAULT_EXIT_TIMEOUT))); |
176 | | - didSurvive = processes.filter((_, i) => !exited[i]); |
177 | | - } catch (error) { |
178 | | - didSurvive = processes; |
179 | | - hadError = error; |
| 157 | + // Search by port |
| 158 | + if (term.startsWith(':')) { |
| 159 | + const port = term.slice(1); |
| 160 | + return searchProcessesByPort(filtered, port); |
180 | 161 | } |
181 | 162 |
|
182 | | - if (didSurvive.length === 0) { |
183 | | - return; |
| 163 | + // Search by PID |
| 164 | + const pidMatch = searchProcessByPid(filtered, term); |
| 165 | + if (pidMatch) { |
| 166 | + return [pidMatch]; |
184 | 167 | } |
185 | 168 |
|
186 | | - const suffix = didSurvive.length > 1 ? 'es' : ''; |
187 | | - const problemText = hadError ? `Error killing process${suffix}.` : `Process${suffix} didn't exit in ${DEFAULT_EXIT_TIMEOUT}ms.`; |
| 169 | + // Search by name |
| 170 | + return searchProcessesByName(filtered, term, searcher); |
| 171 | +}; |
| 172 | + |
| 173 | +const handleFkillError = async processes => { |
| 174 | + const shouldForceKill = await promptForceKill(processes, 'Error killing process.'); |
188 | 175 |
|
| 176 | + if (shouldForceKill) { |
| 177 | + await fkill(processes, { |
| 178 | + force: true, |
| 179 | + ignoreCase: true, |
| 180 | + }); |
| 181 | + } |
| 182 | +}; |
| 183 | + |
| 184 | +const DEFAULT_EXIT_TIMEOUT = 3000; |
| 185 | + |
| 186 | +const attemptKillProcesses = async processes => { |
| 187 | + try { |
| 188 | + await fkill(processes); |
| 189 | + const exitStatuses = await Promise.all(processes.map(process_ => processExited(process_, DEFAULT_EXIT_TIMEOUT))); |
| 190 | + const survivors = processes.filter((_, index) => !exitStatuses[index]); |
| 191 | + return {survivors, hadError: false}; |
| 192 | + } catch { |
| 193 | + return {survivors: processes, hadError: true}; |
| 194 | + } |
| 195 | +}; |
| 196 | + |
| 197 | +const promptForceKill = async (survivingProcesses, message) => { |
189 | 198 | if (process.stdout.isTTY === false) { |
190 | | - console.error(`${problemText} Try \`fkill --force ${didSurvive.join(' ')}\``); |
| 199 | + console.error(`${message} Try \`fkill --force ${survivingProcesses.join(' ')}\``); |
191 | 200 | process.exit(1); // eslint-disable-line unicorn/no-process-exit |
192 | 201 | } |
193 | 202 |
|
194 | 203 | const answer = await inquirer.prompt([{ |
195 | 204 | type: 'confirm', |
196 | 205 | name: 'forceKill', |
197 | | - message: `${problemText} Would you like to use the force?`, |
| 206 | + message: `${message} Would you like to use the force?`, |
198 | 207 | }]); |
199 | 208 |
|
200 | | - if (!answer.forceKill) { |
| 209 | + return answer.forceKill; |
| 210 | +}; |
| 211 | + |
| 212 | +const performKillSequence = async processes => { |
| 213 | + const processList = Array.isArray(processes) ? processes : [processes]; |
| 214 | + const {survivors, hadError} = await attemptKillProcesses(processList); |
| 215 | + |
| 216 | + if (survivors.length === 0) { |
201 | 217 | return; |
202 | 218 | } |
203 | 219 |
|
204 | | - await fkill(processes, { |
205 | | - force: true, |
206 | | - ignoreCase: true, |
207 | | - }); |
| 220 | + const suffix = survivors.length > 1 ? 'es' : ''; |
| 221 | + const message = hadError ? `Error killing process${suffix}.` : `Process${suffix} didn't exit in ${DEFAULT_EXIT_TIMEOUT}ms.`; |
| 222 | + const shouldForceKill = await promptForceKill(survivors, message); |
| 223 | + |
| 224 | + if (shouldForceKill) { |
| 225 | + await fkill(processList, { |
| 226 | + force: true, |
| 227 | + ignoreCase: true, |
| 228 | + }); |
| 229 | + } |
| 230 | +}; |
| 231 | + |
| 232 | +const findPortsForProcess = (processId, portToPidMap) => { |
| 233 | + const ports = []; |
| 234 | + |
| 235 | + for (const [port, pid] of portToPidMap.entries()) { |
| 236 | + if (processId === pid) { |
| 237 | + ports.push(String(port)); |
| 238 | + } |
| 239 | + } |
| 240 | + |
| 241 | + return ports; |
208 | 242 | }; |
209 | 243 |
|
210 | 244 | const listProcesses = async (processes, flags) => { |
211 | | - inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt); |
| 245 | + const memoryThreshold = flags.verbose ? 0 : 1; |
| 246 | + const cpuThreshold = flags.verbose ? 0 : 3; |
| 247 | + const searcher = new FuzzySearch(processes, ['name'], {caseSensitive: false}); |
212 | 248 |
|
213 | | - const answer = await inquirer.prompt([{ |
214 | | - name: 'processes', |
| 249 | + const selectedPid = await search({ |
215 | 250 | message: 'Running processes:', |
216 | | - type: 'autocomplete', |
217 | 251 | pageSize: 10, |
218 | | - source: async (answers, input) => filterProcesses(input, processes, flags), |
219 | | - }]); |
| 252 | + async source(term = '') { |
| 253 | + const matchingProcesses = filterAndSortProcesses(processes, term, searcher); |
| 254 | + return matchingProcesses.map(process_ => renderProcessForDisplay(process_, flags, memoryThreshold, cpuThreshold)); |
| 255 | + }, |
| 256 | + }); |
220 | 257 |
|
221 | | - performKillSequence(answer.processes); |
| 258 | + performKillSequence(selectedPid); |
222 | 259 | }; |
223 | 260 |
|
224 | 261 | const init = async flags => { |
225 | 262 | escExit(); |
226 | 263 |
|
227 | | - const getPortsFromPid = (value, list) => { |
228 | | - const ports = []; |
229 | | - |
230 | | - for (const [key, listValue] of list.entries()) { |
231 | | - if (value === listValue) { |
232 | | - ports.push(String(key)); |
233 | | - } |
234 | | - } |
235 | | - |
236 | | - return ports; |
237 | | - }; |
238 | | - |
239 | | - const [pids, processes] = await Promise.all([ |
| 264 | + const [portToPidMap, processes] = await Promise.all([ |
240 | 265 | allPortsWithPid(), |
241 | 266 | psList({all: false}), |
242 | 267 | ]); |
243 | 268 |
|
244 | | - const procs = processes.map(process_ => ({...process_, ports: getPortsFromPid(process_.pid, pids)})); |
245 | | - listProcesses(procs, flags); |
| 269 | + const processesWithPorts = processes.map(process_ => ({ |
| 270 | + ...process_, |
| 271 | + ports: findPortsForProcess(process_.pid, portToPidMap), |
| 272 | + })); |
| 273 | + |
| 274 | + listProcesses(processesWithPorts, flags); |
246 | 275 | }; |
247 | 276 |
|
248 | 277 | export {init, handleFkillError}; |
0 commit comments