diff --git a/Extension/package.json b/Extension/package.json index a1e2633e1..569e740d4 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -4673,6 +4673,33 @@ } ] }, + "waitFor": { + "description": "%c_cpp.debuggers.waitFor.description%", + "type": "object", + "default": {}, + "properties": { + "enabled": { + "type": "boolean", + "description": "%c_cpp.debuggers.waitFor.enabled.description%", + "default": false + }, + "pattern": { + "type": "string", + "description": "%c_cpp.debuggers.waitFor.pattern.description%", + "default": "" + }, + "timeout": { + "type": "number", + "description": "%c_cpp.debuggers.waitFor.timeout.description%", + "default": 30000 + }, + "interval": { + "type": "number", + "description": "%c_cpp.debuggers.waitFor.interval.description%", + "default": 150 + } + } + }, "filterStdout": { "type": "boolean", "description": "%c_cpp.debuggers.filterStdout.description%", diff --git a/Extension/package.nls.json b/Extension/package.nls.json index b2ec44040..0d957b167 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -974,6 +974,10 @@ "c_cpp.debuggers.VSSymbolOptionsModuleFilter.excludedModules.description": "Array of modules that the debugger should NOT load symbols for. Wildcards (example: MyCompany.*.dll) are supported.\n\nThis property is ignored unless 'mode' is set to 'loadAllButExcluded'.", "c_cpp.debuggers.VSSymbolOptionsModuleFilter.includedModules.description": "Array of modules that the debugger should load symbols for. Wildcards (example: MyCompany.*.dll) are supported.\n\nThis property is ignored unless 'mode' is set to 'loadOnlyIncluded'.", "c_cpp.debuggers.VSSymbolOptionsModuleFilter.includeSymbolsNextToModules.description": "If true, for any module NOT in the 'includedModules' array, the debugger will still check next to the module itself and the launching executable, but it will not check paths on the symbol search list. This option defaults to 'true'.\n\nThis property is ignored unless 'mode' is set to 'loadOnlyIncluded'.", + "c_cpp.debuggers.waitFor.enabled.description": "If true, the debugger will wait till timeout to match a process cmd pattern and attach to it. If matched, sends a SIGSTOP to the process before attaching.", + "c_cpp.debuggers.waitFor.pattern.description": "The process cmd pattern to match against. The pattern is a regular expression that will be matched against the process name.", + "c_cpp.debuggers.waitFor.timeout.description": "The time, in milliseconds, to wait for a process to match. The default is 10000ms.", + "c_cpp.debuggers.waitFor.interval.description": "The interval, in milliseconds, to wait between checks for the process to match. The default is 200ms.", "c_cpp.semanticTokenTypes.referenceType.description": "Style for C++/CLI reference types.", "c_cpp.semanticTokenTypes.cliProperty.description": "Style for C++/CLI properties.", "c_cpp.semanticTokenTypes.genericType.description": "Style for C++/CLI generic types.", diff --git a/Extension/src/Debugger/attachWaitFor.ts b/Extension/src/Debugger/attachWaitFor.ts new file mode 100644 index 000000000..e3a6dbac4 --- /dev/null +++ b/Extension/src/Debugger/attachWaitFor.ts @@ -0,0 +1,134 @@ + +import * as os from 'os'; +import * as vscode from 'vscode'; +import { localize } from 'vscode-nls'; +import * as util from '../common'; +import { sleep } from '../Utility/Async/sleep'; +import { CimAttachItemsProvider, PsAttachItemsProvider, WmicAttachItemsProvider } from './nativeAttach'; + +export interface WaitForProcessProvider { + poll(program: string, timeout: number, interval: number, token?: vscode.CancellationToken): Promise +} + +export class PollProcessProviderFactory { + static Get(): WaitForProcessProvider { + if (os.platform() === 'win32') { + const pwsh: string | undefined = util.findPowerShell(); + let itemsProvider = pwsh ? new CimAttachItemsProvider(pwsh) : new WmicAttachItemsProvider(); + return new PollWindowsProvider(itemsProvider); + } else { + // Linux and MacOS + return new PollProcProvider(new PsAttachItemsProvider()); + } + } +} + +export class PollProcProvider implements WaitForProcessProvider { + + constructor(itemsProvider: PsAttachItemsProvider) { + this.itemsProvider = itemsProvider; + } + + private itemsProvider: PsAttachItemsProvider; + + async poll(program: string, timeout: number, interval: number, token?: vscode.CancellationToken): Promise { + return new Promise(async (resolve, reject) => { + const startTime = Date.now(); // Get the current time in milliseconds + let process: string | undefined; + while (true) { + let elapsedTime = Date.now() - startTime; + if (elapsedTime >= timeout) { + reject(new Error(localize("waitfor.timeout", "Timeout reached. No process matched the pattern."))); + } + + if (token?.isCancellationRequested) { + reject(new Error(localize("waitfor.cancelled", "Operation cancelled."))); + } + + let procs = await this.itemsProvider.getAttachItems(token) + for (const proc of procs) { + if (proc.detail?.includes(program)) { + process = proc.id + break + } + } + + if (process) { + await util.execChildProcess(`kill -STOP ${process}`, undefined, undefined); + break + } + + sleep(interval) + } + + resolve(process) + }) + } +} + +export class PollWindowsProvider implements WaitForProcessProvider { + constructor(itemsProvider: CimAttachItemsProvider | WmicAttachItemsProvider) { + this.itemsProvider = itemsProvider; + } + + private itemsProvider: CimAttachItemsProvider | WmicAttachItemsProvider; + + public async poll(program: string, timeout: number, interval: number, token?: vscode.CancellationToken): Promise { + return new Promise(async (resolve, reject) => { + const startTime = Date.now(); // Get the current time in milliseconds + let process: string | undefined; + while (true) { + const elapsedTime = Date.now() - startTime; + if (elapsedTime >= timeout) { + reject(new Error(localize("waitfor.timeout", "Timeout reached. No process matched the pattern."))); + } + + // Check for cancellation + if (token?.isCancellationRequested) { + reject(new Error(localize("waitfor.cancelled", "Operation cancelled."))); + } + + let procs = await this.itemsProvider.getAttachItems(token) + for (const proc of procs) { + if (proc.detail?.includes(program)) { + process = proc.id + break + } + } + + if (process) { + // Use pssupend to send SIGSTOP analogous in Windows + await util.execChildProcess(`pssuspend.exe /accepteula -nobanner ${process}`, undefined, undefined) + break + } + + sleep(interval) + } + resolve(process) + }) + } +} + + +export class AttachWaitFor { + constructor(poller: WaitForProcessProvider) { + //this._channel = vscode.window.createOutputChannel('waitfor-attach'); + this.poller = poller; + + } + + // Defaults: ms + private timeout: number = 10000; + private interval: number = 150; + private poller: WaitForProcessProvider; + + public async WaitForProcess(program: string, timeout: number, interval: number, token?: vscode.CancellationToken): Promise { + if (timeout) { + this.timeout = timeout; + } + if (interval) { + this.interval = interval; + } + return await this.poller.poll(program, this.timeout, this.interval, token); + } +} diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index f4f874501..9c8c6cf58 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -22,6 +22,7 @@ import { PlatformInformation } from '../platform'; import { rsync, scp, ssh } from '../SSH/commands'; import * as Telemetry from '../telemetry'; import { AttachItemsProvider, AttachPicker, RemoteAttachPicker } from './attachToProcess'; +import { AttachWaitFor, PollProcessProviderFactory, WaitForProcessProvider } from './attachWaitFor'; import { ConfigMenu, ConfigMode, ConfigSource, CppDebugConfiguration, DebuggerEvent, DebuggerType, DebugType, IConfiguration, IConfigurationSnippet, isDebugLaunchStr, MIConfigurations, PipeTransportConfigurations, TaskStatus, WindowsConfigurations, WSLConfigurations } from './configurations'; import { NativeAttachItemsProviderFactory } from './nativeAttach'; import { Environment, ParsedEnvironmentFile } from './ParsedEnvironmentFile'; @@ -347,16 +348,33 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv } } - // Pick process if process id is empty if (config.request === "attach" && !config.processId) { let processId: string | undefined; - if (config.pipeTransport || config.useExtendedRemote) { - const remoteAttachPicker: RemoteAttachPicker = new RemoteAttachPicker(); - processId = await remoteAttachPicker.ShowAttachEntries(config); + if (config.waitFor.enabled) { + const pollProvider: WaitForProcessProvider = PollProcessProviderFactory.Get(); + const waitForAttach: AttachWaitFor = new AttachWaitFor(pollProvider); + if (config.waitFor.process === "") { + void logger.getOutputChannelLogger().showErrorMessage(localize("waitfor.process.empty", "Wait for Process name is empty.")); + return undefined; + } + if (config.waitFor.timeout < 0) { + void logger.getOutputChannelLogger().showErrorMessage(localize("waitfor.timeout.value", "Wait for Timeout value should be non zero.")); + return undefined; + } + if (config.waitFor.interval < 0) { + void logger.getOutputChannelLogger().showErrorMessage(localize("waitfor.interval.value", "Wait for Interval value should be non zero.")); + return undefined; + } + processId = await waitForAttach.WaitForProcess(config.waitFor.process, config.waitFor.timeout, config.waitFor.interval, token); } else { - const attachItemsProvider: AttachItemsProvider = NativeAttachItemsProviderFactory.Get(); - const attacher: AttachPicker = new AttachPicker(attachItemsProvider); - processId = await attacher.ShowAttachEntries(token); + if (config.pipeTransport || config.useExtendedRemote) { + const remoteAttachPicker: RemoteAttachPicker = new RemoteAttachPicker(); + processId = await remoteAttachPicker.ShowAttachEntries(config); + } else { + const attachItemsProvider: AttachItemsProvider = NativeAttachItemsProviderFactory.Get(); + const attacher: AttachPicker = new AttachPicker(attachItemsProvider); + processId = await attacher.ShowAttachEntries(token); + } } if (processId) { diff --git a/Extension/src/Debugger/configurations.ts b/Extension/src/Debugger/configurations.ts index 96895c6da..47a07636d 100644 --- a/Extension/src/Debugger/configurations.ts +++ b/Extension/src/Debugger/configurations.ts @@ -97,7 +97,7 @@ function createLaunchString(name: string, type: string, executable: string): str "stopAtEntry": false, "cwd": "$\{fileDirname\}", "environment": [], -${ type === "cppdbg" ? `"externalConsole": false` : `"console": "externalTerminal"` } +${type === "cppdbg" ? `"externalConsole": false` : `"console": "externalTerminal"`} `; } @@ -119,6 +119,7 @@ function createRemoteAttachString(name: string, type: string, executable: string `; } + function createPipeTransportString(pipeProgram: string, debuggerProgram: string, pipeArgs: string[] = []): string { return ` "pipeTransport": { @@ -164,7 +165,7 @@ export class MIConfigurations extends Configuration { \t${indentJsonString(createLaunchString(name, this.miDebugger, this.executable))}, \t"MIMode": "${this.MIMode}"{0}{1} }`, [this.miDebugger === "cppdbg" && os.platform() === "win32" ? `,${os.EOL}\t"miDebuggerPath": "/path/to/gdb"` : "", - this.additionalProperties ? `,${os.EOL}\t${indentJsonString(this.additionalProperties)}` : ""]); + this.additionalProperties ? `,${os.EOL}\t${indentJsonString(this.additionalProperties)}` : ""]); return { "label": configPrefix + name, @@ -182,7 +183,7 @@ export class MIConfigurations extends Configuration { \t${indentJsonString(createAttachString(name, this.miDebugger, this.executable))} \t"MIMode": "${this.MIMode}"{0}{1} }`, [this.miDebugger === "cppdbg" && os.platform() === "win32" ? `,${os.EOL}\t"miDebuggerPath": "/path/to/gdb"` : "", - this.additionalProperties ? `,${os.EOL}\t${indentJsonString(this.additionalProperties)}` : ""]); + this.additionalProperties ? `,${os.EOL}\t${indentJsonString(this.additionalProperties)}` : ""]); return { "label": configPrefix + name, diff --git a/Extension/src/Debugger/extension.ts b/Extension/src/Debugger/extension.ts index f784c1382..67ccfb376 100644 --- a/Extension/src/Debugger/extension.ts +++ b/Extension/src/Debugger/extension.ts @@ -125,6 +125,10 @@ export async function initialize(context: vscode.ExtensionContext): Promise 0) { + if (stderr.indexOf('screen size is bogus') >= 0) { + // ignore this error silently; see https://github.com/microsoft/vscode/issues/75932 + // see similar fix for the Node - Debug (Legacy) Extension at https://github.com/microsoft/vscode-node-debug/commit/5298920 + } else { + reject(error); + return; + } } - if (stderr && stderr.length > 0) { - reject(new Error(stderr)); + if (error) { + reject(error); return; } diff --git a/Extension/tools/OptionsSchema.json b/Extension/tools/OptionsSchema.json index 77cc7f2d3..090b5eec1 100644 --- a/Extension/tools/OptionsSchema.json +++ b/Extension/tools/OptionsSchema.json @@ -595,6 +595,28 @@ }, "default": [] }, + "WaitFor": { + "type": "object", + "description": "%c_cpp.debuggers.waitFor.description%", + "default": {}, + "properties": { + "timeout": { + "type": "integer", + "description": "%c_cpp.debuggers.waitFor.timeout.description%", + "default": 10000 + }, + "interval": { + "type": "integer", + "description": "%c_cpp.debuggers.waitFor.interval.description%", + "default": 150 + }, + "pattern": { + "type": "string", + "description": "%c_cpp.debuggers.waitFor.pattern.description%", + "default": "" + } + } + }, "CppdbgLaunchOptions": { "type": "object", "required": [ @@ -936,6 +958,10 @@ } ] }, + "waitFor": { + "$ref": "#/definitions/WaitFor", + "description": "%c_cpp.debuggers.waitFor.description%" + }, "logging": { "$ref": "#/definitions/Logging", "description": "%c_cpp.debuggers.logging.description%" diff --git a/Extension/yarn.lock b/Extension/yarn.lock index c35c50027..aa1f4713f 100644 --- a/Extension/yarn.lock +++ b/Extension/yarn.lock @@ -55,10 +55,10 @@ resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha1-3mM9s+wu9qPIni8ZA4Bj6KEi4sI= -"@github/copilot-language-server@^1.266.0": - version "1.273.0" - resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/@github/copilot-language-server/-/copilot-language-server-1.273.0.tgz#561094fc0d832acae173c40549cd95d27a648fbc" - integrity sha1-VhCU/A2DKsrhc8QFSc2V0npkj7w= +"@github/copilot-language-server@^1.316.0": + version "1.316.0" + resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/@github/copilot-language-server/-/copilot-language-server-1.316.0.tgz#7df2f3857f8e148de36b232ccf8b762dfbb2ec9b" + integrity sha1-ffLzhX+OFI3jayMsz4t2Lfuy7Js= dependencies: vscode-languageserver-protocol "^3.17.5" @@ -5079,10 +5079,10 @@ vinyl@^3.0.0: replace-ext "^2.0.0" teex "^1.0.1" -vscode-cpptools@^6.1.0: - version "6.1.0" - resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/vscode-cpptools/-/vscode-cpptools-6.1.0.tgz#d89bb225f91da45dbee6acbf45f6940aa3926df1" - integrity sha1-2JuyJfkdpF2+5qy/RfaUCqOSbfE= +vscode-cpptools@^6.3.0: + version "6.3.0" + resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/vscode-cpptools/-/vscode-cpptools-6.3.0.tgz#671243ac977d9a6b4ffdcf0e4090075af60c6051" + integrity sha1-ZxJDrJd9mmtP/c8OQJAHWvYMYFE= vscode-jsonrpc@8.1.0: version "8.1.0" @@ -5427,4 +5427,4 @@ yn@3.1.1: yocto-queue@^0.1.0: version "0.1.0" resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha1-ApTrPe4FAo0x7hpfosVWpqrxChs= + integrity sha1-ApTrPe4FAo0x7hpfosVWpqrxChs= \ No newline at end of file