From 41584ad586d3721924ff9f72e917456f64efbe22 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 | 29 +++++++++++++++++++++++ 3 files changed, 35 insertions(+) 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..829c2dd52 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,27 @@ 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(`Invalid processFilter regular expression: ${processFilter}`); + } + + return processes.filter((p: AttachItem) => { + const label: string = p.label ?? ""; + const description: string = p.description ?? ""; + const detail: string = p.detail ?? ""; + return processRegex.test(label) || processRegex.test(description) || processRegex.test(detail); + }); + } + // 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 = `'`; From 2f7393ef9726a07763bd9ca7d79f92611e65ec23 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 | 24 ++--------------- Extension/src/Debugger/processFilter.ts | 32 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 22 deletions(-) create mode 100644 Extension/src/Debugger/processFilter.ts diff --git a/Extension/src/Debugger/attachToProcess.ts b/Extension/src/Debugger/attachToProcess.ts index 829c2dd52..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,27 +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(`Invalid processFilter regular expression: ${processFilter}`); - } - - return processes.filter((p: AttachItem) => { - const label: string = p.label ?? ""; - const description: string = p.description ?? ""; - const detail: string = p.detail ?? ""; - return processRegex.test(label) || processRegex.test(description) || processRegex.test(detail); - }); - } - // 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..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); + }); +} From 16a545f3335ef05f1721e2d7d467d88a9c48b6a6 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 | 49 +++++++++++++++++++++++ 1 file changed, 49 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..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/); + }); +});