From ff0ee74b9d256b9dfea4ed964524a1e07d7881dc Mon Sep 17 00:00:00 2001 From: Adrian Freihofer Date: Fri, 14 Aug 2026 22:45:37 +0200 Subject: [PATCH 1/3] Add processFilter for remote attach process selection When attaching to a process on a remote target, the process always has to be selected by hand, even though the launch configuration already knows which executable it belongs to. A generated configuration cannot hard-code processId either, because the pid changes on every boot and on every restart of the service, so the picker is the only option. Add an optional processFilter regular expression to the cppdbg attach configuration. When set, it is matched against the label, description and detail of the remote process list: exactly one match attach to that process directly more than one show the picker with only the matching entries no match show the full picker, as before All three fields are considered because the item format depends on the transport: useExtendedRemote reports the user and the full command line in the label, while pipeTransport reports the process name in the label and the command line in the detail. An invalid regular expression is reported instead of being silently ignored. This affects remote attach only (pipeTransport and useExtendedRemote); local attach continues to use program-based matching. Closes #14682 --- Extension/package.json | 5 +++++ Extension/package.nls.json | 1 + Extension/src/Debugger/attachToProcess.ts | 27 +++++++++++++++++++++++ Extension/tools/OptionsSchema.json | 5 +++++ 4 files changed, 38 insertions(+) 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..9d81746a9 100644 --- a/Extension/src/Debugger/attachToProcess.ts +++ b/Extension/src/Debugger/attachToProcess.ts @@ -94,6 +94,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 = this.getMatchingProcessesFromConfig(processes, config); + if (matchingProcesses?.length === 1) { + return matchingProcesses[0].id; + } + if (matchingProcesses && matchingProcesses.length > 1) { + processes = matchingProcesses; + } + const attachPickOptions: vscode.QuickPickOptions = { matchOnDetail: true, matchOnDescription: true, @@ -108,6 +116,25 @@ export class RemoteAttachPicker { } } + private getMatchingProcessesFromConfig(processes: AttachItem[], config: any): AttachItem[] | undefined { + const processFilter: string | undefined = typeof config?.processFilter === 'string' ? config.processFilter.trim() : undefined; + if (!processFilter) { + 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 processes.filter((item: AttachItem) => { + return [item.label, item.description, item.detail] + .some((value: string | undefined) => typeof value === "string" && processRegex.test(value)); + }); + } + // Creates a string to run on the host machine which will execute a shell script on the remote machine to retrieve OS and processes private getRemoteProcessCommand(quoteArgs: boolean): string { let innerQuote: string = `'`; 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%", From db17d41d81a2b3541dc0b97106544fc6e5d9992d Mon Sep 17 00:00:00 2001 From: Adrian Freihofer Date: Fri, 14 Aug 2026 22:45:37 +0200 Subject: [PATCH 2/3] Extract remote process filtering into a helper Move the matching logic out of RemoteAttachPicker into a standalone function so that it can be unit tested without a VS Code quick pick or a live connection to a remote target. No functional change. --- Extension/src/Debugger/attachToProcess.ts | 22 ++------------- Extension/src/Debugger/processFilter.ts | 34 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 Extension/src/Debugger/processFilter.ts diff --git a/Extension/src/Debugger/attachToProcess.ts b/Extension/src/Debugger/attachToProcess.ts index 9d81746a9..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,7 +95,7 @@ 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 = this.getMatchingProcessesFromConfig(processes, config); + const matchingProcesses: AttachItem[] | undefined = filterProcessItems(processes, config?.processFilter); if (matchingProcesses?.length === 1) { return matchingProcesses[0].id; } @@ -116,25 +117,6 @@ export class RemoteAttachPicker { } } - private getMatchingProcessesFromConfig(processes: AttachItem[], config: any): AttachItem[] | undefined { - const processFilter: string | undefined = typeof config?.processFilter === 'string' ? config.processFilter.trim() : undefined; - if (!processFilter) { - 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 processes.filter((item: AttachItem) => { - return [item.label, item.description, item.detail] - .some((value: string | undefined) => typeof value === "string" && processRegex.test(value)); - }); - } - // Creates a string to run on the host machine which will execute a shell script on the remote machine to retrieve OS and processes private getRemoteProcessCommand(quoteArgs: boolean): string { let innerQuote: string = `'`; 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)); + }); +} From a98898dd68448e3bd9060891d50a99a70bfb3c76 Mon Sep 17 00:00:00 2001 From: Adrian Freihofer Date: Fri, 14 Aug 2026 22:45:37 +0200 Subject: [PATCH 3/3] Add unit tests for processFilter matching Cover empty and non-string filter values, matching against label, description and detail, multiple matches, and an invalid regular expression. --- Extension/test/unit/processFilter.test.ts | 53 +++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 Extension/test/unit/processFilter.test.ts 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/); + }); +});