diff --git a/Extension/package.json b/Extension/package.json index 5c42a5ecd..03070d51e 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -5260,6 +5260,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..c42e03e45 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. If no process matches, the full process picker is shown. An invalid regular expression reports an error.", "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..155d03af3 --- /dev/null +++ b/Extension/src/Debugger/processFilter.ts @@ -0,0 +1,34 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as nls from 'vscode-nls'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +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. + if (typeof processFilter !== 'string' || !processFilter.trim()) { + return undefined; + } + + let processRegex: RegExp; + try { + processRegex = new RegExp(processFilter); + } catch { + throw new Error(localize("invalid.processFilter.regex", "Invalid {0} regular expression: {1}", "processFilter", processFilter)); + } + + return items.filter((item: T) => { + return [item.label, item.description, item.detail] + .some((value: string | undefined) => typeof value === "string" && processRegex.test(value)); + }); +} diff --git a/Extension/test/unit/processFilter.test.ts b/Extension/test/unit/processFilter.test.ts new file mode 100644 index 000000000..30ba1016a --- /dev/null +++ b/Extension/test/unit/processFilter.test.ts @@ -0,0 +1,53 @@ +/* -------------------------------------------------------------------------------------------- + * 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('preserves edge whitespace in the regular expression', () => { + deepStrictEqual(filterProcessItems(processes, '^root /usr/bin/my-daemon --serve ')?.map(p => p.id), []); + }); + + 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/); + }); +}); diff --git a/Extension/tools/OptionsSchema.json b/Extension/tools/OptionsSchema.json index 010af9a76..644f28a32 100644 --- a/Extension/tools/OptionsSchema.json +++ b/Extension/tools/OptionsSchema.json @@ -923,6 +923,11 @@ } ] }, + "processFilter": { + "type": "string", + "description": "%c_cpp.debuggers.processFilter.description%", + "default": "" + }, "filterStdout": { "type": "boolean", "description": "%c_cpp.debuggers.filterStdout.description%",