diff --git a/Extension/package.json b/Extension/package.json index e4f44941d..b0341b65d 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -5259,6 +5259,11 @@ } ] }, + "processFilter": { + "type": "string", + "description": "%c_cpp.debuggers.processFilter.description%", + "default": "" + }, "filterStdout": { "type": "boolean", "description": "%c_cpp.debuggers.filterStdout.description%", diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 686234a1f..c46813a86 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -982,6 +982,7 @@ "{Locked=\"`${command:pickProcess}`\"}" ] }, + "c_cpp.debuggers.processFilter.description": "Optional regular expression used to match remote attach candidates by label, description, or detail. If exactly one process matches, the debugger attaches automatically. If multiple processes match, the process picker is shown with only matching entries.", "c_cpp.debuggers.program.attach.markdownDescription": { "message": "Full path to the program executable. The debugger will search for a running process matching this executable path and attach to it. If multiple processes match, a selection prompt will be shown. This field is required to load debug symbols for the attached process.", "comment": [ diff --git a/Extension/src/Debugger/attachToProcess.ts b/Extension/src/Debugger/attachToProcess.ts index 169c99218..1fcd1e210 100644 --- a/Extension/src/Debugger/attachToProcess.ts +++ b/Extension/src/Debugger/attachToProcess.ts @@ -6,6 +6,7 @@ import { CppSettings } from '../LanguageServer/settings'; import { AttachItem, showQuickPick } from './attachQuickPick'; import { PsProcessParser } from './nativeAttach'; +import { filterProcessItems } from './processFilter'; import * as os from 'os'; import * as path from 'path'; @@ -94,6 +95,14 @@ export class RemoteAttachPicker { throw new Error(localize("no.pipetransport.useextendedremote", "Chosen debug configuration does not contain {0} or {1}", "pipeTransport", "useExtendedRemote")); } + const matchingProcesses: AttachItem[] | undefined = filterProcessItems(processes, config?.processFilter); + if (matchingProcesses?.length === 1) { + return matchingProcesses[0].id; + } + if (matchingProcesses && matchingProcesses.length > 1) { + processes = matchingProcesses; + } + const attachPickOptions: vscode.QuickPickOptions = { matchOnDetail: true, matchOnDescription: true, diff --git a/Extension/src/Debugger/processFilter.ts b/Extension/src/Debugger/processFilter.ts new file mode 100644 index 000000000..67acfea27 --- /dev/null +++ b/Extension/src/Debugger/processFilter.ts @@ -0,0 +1,32 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +export interface ProcessFilterItem { + label?: string; + description?: string; + detail?: string; +} + +export function filterProcessItems(items: T[], processFilter?: unknown): T[] | undefined { + // The value comes from launch.json, so it is not guaranteed to be a string. + const trimmedFilter: string | undefined = typeof processFilter === 'string' ? processFilter.trim() : undefined; + if (!trimmedFilter) { + return undefined; + } + + let processRegex: RegExp; + try { + processRegex = new RegExp(trimmedFilter); + } catch { + throw new Error(`Invalid processFilter regular expression: ${trimmedFilter}`); + } + + return items.filter((item: T) => { + const label: string = item.label ?? ""; + const description: string = item.description ?? ""; + const detail: string = item.detail ?? ""; + return processRegex.test(label) || processRegex.test(description) || processRegex.test(detail); + }); +} diff --git a/Extension/test/unit/processFilter.test.ts b/Extension/test/unit/processFilter.test.ts new file mode 100644 index 000000000..1379aa8ca --- /dev/null +++ b/Extension/test/unit/processFilter.test.ts @@ -0,0 +1,49 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { deepStrictEqual, strictEqual, throws } from 'assert'; +import { describe, it } from 'mocha'; +import { filterProcessItems } from '../../src/Debugger/processFilter'; + +interface TestProcessItem { + label?: string; + description?: string; + detail?: string; + id: string; +} + +describe('Remote attach process filter', () => { + const processes: TestProcessItem[] = [ + { id: '101', label: 'root /usr/bin/my-daemon --serve', description: '101' }, + { id: '102', label: 'root /usr/bin/other-service', description: '102', detail: 'worker' }, + { id: '103', label: 'app /usr/bin/my-daemon --once', description: '103' } + ]; + + it('returns undefined when filter is empty', () => { + strictEqual(filterProcessItems(processes, ''), undefined); + strictEqual(filterProcessItems(processes, ' '), undefined); + strictEqual(filterProcessItems(processes, undefined), undefined); + }); + + it('returns undefined when filter is not a string', () => { + strictEqual(filterProcessItems(processes, 1234), undefined); + strictEqual(filterProcessItems(processes, true), undefined); + strictEqual(filterProcessItems(processes, {}), undefined); + }); + + it('matches by label and description and detail', () => { + deepStrictEqual(filterProcessItems(processes, 'other-service')?.map(p => p.id), ['102']); + deepStrictEqual(filterProcessItems(processes, '^101$')?.map(p => p.id), ['101']); + deepStrictEqual(filterProcessItems(processes, 'worker')?.map(p => p.id), ['102']); + }); + + it('returns multiple matches when regex matches more than one process', () => { + deepStrictEqual(filterProcessItems(processes, 'my-daemon')?.map(p => p.id), ['101', '103']); + }); + + it('throws for invalid regular expression', () => { + throws(() => filterProcessItems(processes, '['), /Invalid processFilter regular expression/); + }); +});