Compare commits

...
Author SHA1 Message Date
Sean McManus 8309a022d9 More comment fixes, mostly with /* blocks. 2025-06-02 17:04:28 -07:00
Sean McManus acaa8d116f Missed some cases. 2025-06-02 16:39:51 -07:00
Sean McManus 18a108a7d2 Fix comment/string casing issues. 2025-06-02 16:35:46 -07:00
Sean McManus 998b2570b8 Fix ; if ( 2025-06-02 15:32:15 -07:00
Sean McManus 23f4d67ed8 Remove "ada" from the vsdbg and cpplldb langauge lists. 2025-05-30 12:25:07 -07:00
Sean McManus e7c7ccd362 Fix /path/to/gdb not being localized. 2025-05-29 16:07:31 -07:00
Sean McManus 01cfad93a2 Fix some comments. 2025-05-29 16:06:36 -07:00
Sean McManus 6532902f76 Merge branch 'main' into dev/garretts/implement-lldb-dap 2025-05-29 14:55:40 -07:00
Sean McManus d225eeb592 Fix stopAtEntry, sourceFileMap, processId. 2025-05-29 14:34:46 -07:00
Sean McManus 10563fad14 Merge branch 'main' into dev/garretts/implement-lldb-dap 2025-05-29 11:19:56 -07:00
Sean McManus 803e8a9c22 Fix string issues. 2025-05-29 11:17:17 -07:00
Sean McManus c5899fa663 Merge branch 'main' into dev/garretts/implement-lldb-dap 2025-05-27 15:07:04 -07:00
Sean McManus 9f99738cb5 Merge branch 'main' into dev/garretts/implement-lldb-dap 2025-05-19 19:26:18 -07:00
Sean McManus 9a3da2b74f Alternative fix for linter errors. 2025-05-19 19:25:51 -07:00
Sean McManus 0e2ddc6954 Move the imports. 2025-05-19 18:10:50 -07:00
Sean McManus 58f5ae3e57 Fix linter errors. 2025-05-19 16:16:34 -07:00
Sean McManus f487c9be4e Fix build errors. 2025-05-19 15:24:47 -07:00
Sean McManus c48a4fb0e0 Merge branch 'main' into dev/garretts/implement-lldb-dap 2025-05-19 14:05:58 -07:00
Bob Brown 54bfc004a5 Latest update from @fearthecowboy. Still need to review lint issues. 2025-05-16 09:43:32 -07:00
Sean McManus b603db62f9 Merge branch 'main' into dev/garretts/implement-lldb-dap 2025-05-05 15:06:58 -07:00
Garrett Serack (from Dev Box) 0aa3fc8804 Merge branch 'main' of https://github.com/microsoft/vscode-cpptools into dev/garretts/implement-lldb-dap 2025-05-02 09:52:19 -07:00
Garrett Serack (from Dev Box) 94f05866a4 Merge branch 'dev/garretts/implement-lldb-dap' of https://github.com/microsoft/vscode-cpptools into dev/garretts/implement-lldb-dap 2025-05-02 09:52:09 -07:00
Garrett Serack (from Dev Box) 5e17eeeeae address feedback 2025-05-02 09:51:59 -07:00
Garrett Serack c983a011bd Merge branch 'main' into dev/garretts/implement-lldb-dap 2025-05-01 07:26:46 -07:00
Garrett Serack (from Dev Box) 30b5be6540 Address feedback 2025-04-30 07:50:29 -07:00
Garrett Serack (from Dev Box) a433293530 Merge branch 'dev/garretts/implement-lldb-dap' of https://github.com/microsoft/vscode-cpptools into dev/garretts/implement-lldb-dap 2025-04-29 12:12:43 -07:00
Garrett Serack (from Dev Box) 9313999c8e fix cppvsdbg console type when generating config 2025-04-29 12:11:35 -07:00
Garrett Serack ba4d4131cd Merge branch 'main' into dev/garretts/implement-lldb-dap 2025-04-29 11:22:24 -07:00
Garrett Serack (from Dev Box) e2d7deb912 Fix test 2025-04-29 10:32:45 -07:00
Garrett Serack (from Dev Box) 22439cccde Merge branch 'main' of https://github.com/microsoft/vscode-cpptools into dev/garretts/implement-lldb-dap 2025-04-29 09:46:22 -07:00
Garrett Serack (from Dev Box) e9b2659e1e Enable using LLDB-DAP to debug 2025-04-29 09:45:45 -07:00
26 changed files with 3908 additions and 557 deletions
@@ -134,6 +134,10 @@ export async function main() {
packageJSON.contributes.debuggers[1].configurationAttributes.launch = schemaJSON.definitions.CppvsdbgLaunchOptions;
packageJSON.contributes.debuggers[1].configurationAttributes.attach = schemaJSON.definitions.CppvsdbgAttachOptions;
// cpplldb
packageJSON.contributes.debuggers[2].configurationAttributes.launch = schemaJSON.definitions.CpplldbLaunchOptions;
packageJSON.contributes.debuggers[2].configurationAttributes.attach = schemaJSON.definitions.CpplldbAttachOptions;
let content: string = JSON.stringify(packageJSON, null, 4);
// We use '\u200b' (unicode zero-length space character) to break VS Code's URL detection regex for URLs that are examples. This process will
+1412 -15
View File
File diff suppressed because it is too large Load Diff
+63
View File
@@ -979,6 +979,69 @@
"c_cpp.debuggers.vsCodeCommand.description": "VS Code command to be invoked. Can be a command in VS Code or an active extension.",
"c_cpp.debuggers.vsCodeCommand.command.description": "VS Code command to be invoked.",
"c_cpp.debuggers.vsCodeCommand.args.description": "Arguments to the VS Code command.",
"c_cpp.debuggers.cpplldb.debuggerPath.description": {
"message": "Full path to the LLDB-DAP debugger executable. If not specified, the extension will search for the debugger in the system PATH.",
"comment": [
"{Locked=\"LLDB-DAP\"} {Locked=\"PATH\"}"
]
},
"c_cpp.debuggers.cpplldb.debuggerArgs.description": {
"message": "Additional arguments for the LLDB-DAP debugger.",
"comment": [
"{Locked=\"LLDB-DAP\"}"
]
},
"c_cpp.debuggers.cpplldb.externalConsole.description": "If true, launch the external console for the debugger.",
"c_cpp.debuggers.cpplldb.disableAslr.description": "Disable Address Space Layout Randomization (if supported).",
"c_cpp.debuggers.cpplldb.disableStdio.description": "Disables the standard input/output streams during debugging.",
"c_cpp.debuggers.cpplldb.expandShellArguments.description": "Expands program arguments as a shell would (does not actually launch the program in a shell).",
"c_cpp.debuggers.cpplldb.detachOnError.description": "Detach the debugger when an error occurs.",
"c_cpp.debuggers.cpplldb.debuggerRoot.description": "The directory where relative object files can be located.",
"c_cpp.debuggers.cpplldb.targetTriple.description": "Overrides the target triple of the target program.",
"c_cpp.debuggers.cpplldb.platformName.description": "Overrides the platform name of the target program.",
"c_cpp.debuggers.cpplldb.initCommands.description": "LLDB commands executed upon debugger startup prior to creating the LLDB target.",
"c_cpp.debuggers.cpplldb.preRunCommands.description": "LLDB commands executed just before launching/attaching, after the LLDB target has been created.",
"c_cpp.debuggers.cpplldb.postRunCommands.description": "LLDB commands executed after launching when it's in a stopped state prior to any automatic continuation.",
"c_cpp.debuggers.cpplldb.launchCommands.description": "Custom LLDB commands that are executed instead of launching a process.",
"c_cpp.debuggers.cpplldb.stopCommands.description": "LLDB commands executed each time the program stops.",
"c_cpp.debuggers.cpplldb.exitCommands.description": "LLDB commands executed when the program exits.",
"c_cpp.debuggers.cpplldb.terminateCommands.description": "LLDB commands executed when the debugging session ends.",
"c_cpp.debuggers.cpplldb.enableAutoVariableSummaries.description": "Enable auto-generated summaries for variables when no summaries exist for a given type. This feature can cause performance delays in large projects when viewing variables.",
"c_cpp.debuggers.cpplldb.displayExtendedBacktrace.description": "Enable language-specific extended backtraces.",
"c_cpp.debuggers.cpplldb.enableSyntheticChildDebugging.description": "If a variable is displayed using synthetic children, also display the actual contents of the variable at the end under an entry. This is useful when creating synthetic child plug-ins as it lets you see the actual contents of the variable.",
"c_cpp.debuggers.cpplldb.commandEscapePrefix.description": "The escape prefix character to use for executing regular LLDB commands in the Debug Console, instead of printing variables.",
"c_cpp.debuggers.cpplldb.customFrameFormat.markdownDescription": {
"message": "When specified, stack frames will have descriptions generated based on the provided format. See https://lldb.llvm.org/use/formatting.html.",
"comment": [
"{Locked=\"https://lldb.llvm.org/use/formatting.html\"}"
]
},
"c_cpp.debuggers.cpplldb.customThreadFormat.markdownDescription": {
"message": "When specified, threads will have descriptions generated based on the provided format. See https://lldb.llvm.org/use/formatting.html.",
"comment": [
"{Locked=\"https://lldb.llvm.org/use/formatting.html\"}"
]
},
"c_cpp.debuggers.cpplldb.attachCommands.description": "Custom LLDB commands that are executed instead of attaching to a process ID or to a process by name.",
"c_cpp.debuggers.cpplldb.waitFor.markdownDescription": {
"message": "Wait for the process to launch by looking for a process with a basename that matches the value specified in `program`.",
"comment": [
"{Locked=\"`program`\"}"
]
},
"c_cpp.debuggers.cpplldb.coreFile.description": "Path to the core file to debug.",
"c_cpp.debuggers.cpplldb.gdbRemotePort.markdownDescription": {
"message": "TCP/IP port to attach to a remote system. Specifying both `processId` and `port` is an error.",
"comment": [
"{Locked=\"`processId`\"} {Locked=\"`port`\"}"
]
},
"c_cpp.debuggers.cpplldb.gdbRemoteHost.markdownDescription": {
"message": "The hostname to connect to a remote system. The default hostname is `localhost`.",
"comment": [
"{Locked=\"`localhost`\"}"
]
},
"c_cpp.taskDefinitions.name.description": "The name of the task.",
"c_cpp.taskDefinitions.command.description": "The path to either a compiler or script that performs compilation.",
"c_cpp.taskDefinitions.args.description": "Additional arguments to pass to the compiler or compilation script.",
@@ -28,6 +28,10 @@ class RefreshButton implements vscode.QuickInputButton {
export interface AttachItem extends vscode.QuickPickItem {
id?: string;
name?: string;
pid?: string;
commandLine?: string;
fullPath?: string;
}
// We should not await on this function.
+43 -4
View File
@@ -12,6 +12,7 @@ import * as path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as util from '../common';
import { executableName, ProcessReturnType, spawnChildProcess } from '../common-remote-safe';
import * as debugUtils from './utils';
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
@@ -21,6 +22,28 @@ export interface AttachItemsProvider {
getAttachItems(token?: vscode.CancellationToken): Promise<AttachItem[]>;
}
/**
* Determines if a process matches the specified filter criteria.
*
* This function checks if the given process name or its full path matches
* the provided filter string. It also normalizes the filter string to handle
* relative paths and compares the executable name derived from the filter
* with the executable name of the process.
*
* @param filterTo - The filter string to match against. This can be a process name,
* an executable name, or a file path.
* @param processName - The name of the process to check.
* @param fullPath - The full path of the process executable, if available.
* @returns `true` if the process matches the filter criteria; otherwise, `false`.
*/
export function processMatches(filterTo: string, processName: string, fullPath: string | undefined) {
const normalized = path.resolve(filterTo);
const executable = executableName(filterTo);
// Filter out the processes that do not somehow match.
return processName === filterTo || executableName(processName) === executable || fullPath === filterTo || fullPath === normalized;
}
export class AttachPicker {
constructor(private attachItemsProvider: AttachItemsProvider) { }
@@ -28,6 +51,23 @@ export class AttachPicker {
public async ShowAttachEntries(token?: vscode.CancellationToken): Promise<string | undefined> {
return showQuickPick(() => this.attachItemsProvider.getAttachItems(token));
}
/** Shows the Attach QuickPick, but if a valid filterTo process name/path is passed in, it only shows those entries. */
public async ShowAttachEntriesFiltered(filterTo?: string, token?: vscode.CancellationToken): Promise<string | undefined> {
return showQuickPick(async () => {
const items = await this.attachItemsProvider.getAttachItems(token);
if (filterTo) {
// Filter out the processes that do not somehow match.
const filtered = items.filter(each => processMatches(filterTo, each.label, each.fullPath));
if (filtered.length > 0) {
// Only filter the list if we have actual matches.
return filtered;
}
}
return items;
});
}
}
export class RemoteAttachPicker {
@@ -177,18 +217,17 @@ export class RemoteAttachPicker {
return 0;
}
return aLower < bLower ? -1 : 1;
})
.map(p => p.toAttachItem());
});
}
}
}
private async getRemoteProcessesExtendedRemote(miDebuggerPath: string, miDebuggerServerAddress: string): Promise<AttachItem[]> {
const args: string[] = [`-ex "target extended-remote ${miDebuggerServerAddress}"`, '-ex "info os processes"', '-batch'];
let processListOutput: util.ProcessReturnType = await util.spawnChildProcess(miDebuggerPath, args);
let processListOutput: ProcessReturnType = await spawnChildProcess(miDebuggerPath, args);
// The device may not be responsive for a while during the restart after image deploy. Retry 5 times.
for (let i: number = 0; i < 5 && !processListOutput.succeeded && processListOutput.outputError.length === 0; i++) {
processListOutput = await util.spawnChildProcess(miDebuggerPath, args);
processListOutput = await spawnChildProcess(miDebuggerPath, args);
}
if (!processListOutput.succeeded) {
+221 -171
View File
@@ -12,6 +12,7 @@ import { promisify } from 'util';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as util from '../common';
import { executableName, ProcessReturnType, spawnChildProcess } from '../common-remote-safe';
import { isWindows } from '../constants';
import { expandAllStrings, ExpansionOptions, ExpansionVars } from '../expand';
import { CppBuildTask, CppBuildTaskDefinition, cppBuildTaskProvider } from '../LanguageServer/cppBuildTaskProvider';
@@ -21,8 +22,9 @@ import * as logger from '../logger';
import { PlatformInformation } from '../platform';
import { rsync, scp, ssh } from '../SSH/commands';
import * as Telemetry from '../telemetry';
import { AttachItemsProvider, AttachPicker, RemoteAttachPicker } from './attachToProcess';
import { ConfigMenu, ConfigMode, ConfigSource, CppDebugConfiguration, DebuggerEvent, DebuggerType, DebugType, IConfiguration, IConfigurationSnippet, isDebugLaunchStr, MIConfigurations, PipeTransportConfigurations, TaskStatus, WindowsConfigurations, WSLConfigurations } from './configurations';
import { AttachItemsProvider, AttachPicker, processMatches, RemoteAttachPicker } from './attachToProcess';
import { ConfigMenu, ConfigMode, ConfigSource, Configuration, CppDebugConfiguration, DebuggerEvent, DebuggerType, DebugType, IConfigurationSnippet, isDebugLaunchStr, LldbDapConfigurations, MIConfigurations, PipeTransportConfigurations, TaskStatus, WindowsConfigurations, WSLConfigurations } from './configurations';
import { findLldbDap, translateToLldbDap } from './lldb-dap';
import { NativeAttachItemsProviderFactory } from './nativeAttach';
import { Environment, ParsedEnvironmentFile } from './ParsedEnvironmentFile';
import * as debugUtils from './utils';
@@ -48,17 +50,13 @@ const globAsync: (pattern: string, options?: glob.IOptions | undefined) => Promi
*/
export class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
private type: DebuggerType;
private assetProvider: IConfigurationAssetProvider;
// Keep a list of tasks detected by cppBuildTaskProvider.
private static detectedBuildTasks: CppBuildTask[] = [];
private static detectedCppBuildTasks: CppBuildTask[] = [];
private static detectedCBuildTasks: CppBuildTask[] = [];
protected static recentBuildTaskLabel: string;
public constructor(assetProvider: IConfigurationAssetProvider, type: DebuggerType) {
this.assetProvider = assetProvider;
this.type = type;
public constructor(private assetProvider: ConfigurationAssetProvider, private type: DebuggerType) {
}
public static ClearDetectedBuildTasks(): void {
@@ -236,7 +234,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
if (config.type === DebuggerType.cppvsdbg) {
// Fail if cppvsdbg type is running on non-Windows
if (os.platform() !== 'win32') {
void logger.getOutputChannelLogger().showWarningMessage(localize("debugger.not.available", "Debugger of type: '{0}' is only available on Windows. Use type: '{1}' on the current OS platform.", "cppvsdbg", "cppdbg"));
void logger.getOutputChannelLogger().showWarningMessage(localize("debugger.not.available", "Debugger of type: '{0}' is only available on Windows. Use type: '{1}' or '{2}' on the current OS platform.", DebuggerType.cppvsdbg, DebuggerType.cppdbg, DebuggerType.cpplldb));
return undefined; // Abort debugging silently.
}
@@ -269,55 +267,57 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
this.resolveSourceFileMapVariables(config);
// Modify WSL config for OpenDebugAD7
if (os.platform() === 'win32' &&
config.pipeTransport &&
config.pipeTransport.pipeProgram) {
let replacedPipeProgram: string | undefined;
const pipeProgramStr: string = config.pipeTransport.pipeProgram.toLowerCase().trim();
if (config.type === DebuggerType.cppdbg) {
// Modify WSL config for OpenDebugAD7
if (os.platform() === 'win32' &&
config.pipeTransport &&
config.pipeTransport.pipeProgram) {
let replacedPipeProgram: string | undefined;
const pipeProgramStr: string = config.pipeTransport.pipeProgram.toLowerCase().trim();
// OpenDebugAD7 is a 32-bit process. Make sure the WSL pipe transport is using the correct program.
replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(pipeProgramStr, debugUtils.ArchType.ia32);
// OpenDebugAD7 is a 32-bit process. Make sure the WSL pipe transport is using the correct program.
replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(pipeProgramStr, debugUtils.ArchType.ia32);
// If pipeProgram does not get replaced and there is a pipeCwd, concatenate with pipeProgramStr and attempt to replace.
if (!replacedPipeProgram && !path.isAbsolute(pipeProgramStr) && config.pipeTransport.pipeCwd) {
const pipeCwdStr: string = config.pipeTransport.pipeCwd.toLowerCase().trim();
const newPipeProgramStr: string = path.join(pipeCwdStr, pipeProgramStr);
// If pipeProgram does not get replaced and there is a pipeCwd, concatenate with pipeProgramStr and attempt to replace.
if (!replacedPipeProgram && !path.isAbsolute(pipeProgramStr) && config.pipeTransport.pipeCwd) {
const pipeCwdStr: string = config.pipeTransport.pipeCwd.toLowerCase().trim();
const newPipeProgramStr: string = path.join(pipeCwdStr, pipeProgramStr);
replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(newPipeProgramStr, debugUtils.ArchType.ia32);
replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(newPipeProgramStr, debugUtils.ArchType.ia32);
}
if (replacedPipeProgram) {
config.pipeTransport.pipeProgram = replacedPipeProgram;
}
}
if (replacedPipeProgram) {
config.pipeTransport.pipeProgram = replacedPipeProgram;
}
}
const macOSMIMode: string = config.osx?.MIMode ?? config.MIMode;
const macOSMIDebuggerPath: string = config.osx?.miDebuggerPath ?? config.miDebuggerPath;
const macOSMIMode: string = config.osx?.MIMode ?? config.MIMode;
const macOSMIDebuggerPath: string = config.osx?.miDebuggerPath ?? config.miDebuggerPath;
const lldb_mi_10_x_path: string = path.join(util.extensionPath, "debugAdapters", "lldb-mi", "bin", "lldb-mi");
const lldb_mi_10_x_path: string = path.join(util.extensionPath, "debugAdapters", "lldb-mi", "bin", "lldb-mi");
// Validate LLDB-MI
if (os.platform() === 'darwin' && // Check for macOS
fs.existsSync(lldb_mi_10_x_path) && // lldb-mi 10.x exists
(!macOSMIMode || macOSMIMode === 'lldb') &&
!macOSMIDebuggerPath // User did not provide custom lldb-mi
) {
const frameworkPath: string | undefined = this.getLLDBFrameworkPath();
// Validate LLDB-MI
if (os.platform() === 'darwin' && // Check for macOS
fs.existsSync(lldb_mi_10_x_path) && // lldb-mi 10.x exists
(!macOSMIMode || macOSMIMode === 'lldb') &&
!macOSMIDebuggerPath // User did not provide custom lldb-mi
) {
const frameworkPath: string | undefined = this.getLLDBFrameworkPath();
if (!frameworkPath) {
const moreInfoButton: string = localize("lldb.framework.install.xcode", "More Info");
const LLDBFrameworkMissingMessage: string = localize("lldb.framework.not.found", "Unable to locate 'LLDB.framework' for lldb-mi. Please install XCode or XCode Command Line Tools.");
if (!frameworkPath) {
const moreInfoButton: string = localize("lldb.framework.install.xcode", "More Info");
const LLDBFrameworkMissingMessage: string = localize("lldb.framework.not.found", "Unable to locate 'LLDB.framework' for lldb-mi. Please install XCode or XCode Command Line Tools.");
void vscode.window.showErrorMessage(LLDBFrameworkMissingMessage, moreInfoButton)
.then(value => {
if (value === moreInfoButton) {
const helpURL: string = "https://aka.ms/vscode-cpptools/LLDBFrameworkNotFound";
void vscode.env.openExternal(vscode.Uri.parse(helpURL));
}
});
void vscode.window.showErrorMessage(LLDBFrameworkMissingMessage, moreInfoButton)
.then(value => {
if (value === moreInfoButton) {
const helpURL: string = "https://aka.ms/vscode-cpptools/LLDBFrameworkNotFound";
void vscode.env.openExternal(vscode.Uri.parse(helpURL));
}
});
return undefined;
return undefined;
}
}
}
@@ -355,18 +355,69 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
processId = await remoteAttachPicker.ShowAttachEntries(config);
} else {
const attachItemsProvider: AttachItemsProvider = NativeAttachItemsProviderFactory.Get();
const attacher: AttachPicker = new AttachPicker(attachItemsProvider);
processId = await attacher.ShowAttachEntries(token);
// LLDB-DAP supports attaching to process by pid or by name.
// If they do specify 'program', then we can check to see if there
// is a process that matches, or they are using 'waitFor' (on Linux/macOS).
if (config.program) {
// If they specified 'program', get the tasks that match that.
const items = (await attachItemsProvider.getAttachItems(token)).filter(each => processMatches(config.program, each.label, each.fullPath));
switch (items.length) {
case 0:
// No matching processes
if (config.type === DebuggerType.cpplldb && config.waitFor) {
if (isWindows) {
// On Windows, waitFor on lldb-dap is not supported - so we'll let the user know.
void logger.getOutputChannelLogger().showWarningMessage(localize("waitFor.not.supported", "The {0} debugger does not support '{1}' on Windows.", "LLDB-DAP", "waitFor"));
// Drop the waitFor and program (which will just have the picker show all processes).
delete config.program;
delete config.waitFor;
}
// They did specify 'waitFor' so then let's get out fast and let lldb-dap handle it.
logger.note(localize("waiting.for.process", "Waiting to attach process '{0}'", config.program));
translateToLldbDap(config);
Telemetry.logDebuggerEvent(config.request, { "debuggerType": config.type });
return config;
}
// Just let the picker show all processes.
break;
case 1:
// If there is only a single process that matches, auto-select it.
processId = config.processId = items[0].id;
break;
}
}
// If they didn't specify a processId, we'll show the picker.
if (!processId) {
// Show the picker to let the user select a process, but if they did specify a program, we'll narrow the list for them.
const attacher: AttachPicker = new AttachPicker(attachItemsProvider);
processId = await attacher.ShowAttachEntriesFiltered(config.program, token);
}
}
if (processId) {
config.processId = processId;
delete config.program;
} else {
void logger.getOutputChannelLogger().showErrorMessage("No process was selected.");
return undefined;
}
}
// Finish translating the config for lldb-dap.
if (config.type === DebuggerType.cpplldb) {
translateToLldbDap(config);
Telemetry.logDebuggerEvent(config.request, { "debuggerType": config.type });
return config;
}
Telemetry.logDebuggerEvent(config.request, { "debuggerType": config.type, mimode: config.MIMode });
return config;
}
@@ -394,7 +445,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
if (!command) {
return false;
}
if (defaultTemplateConfig.name.startsWith("(Windows) ")) {
if (defaultTemplateConfig?.name.startsWith("(Windows) ")) {
if (command.startsWith("cl.exe")) {
return true;
}
@@ -427,13 +478,17 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
const newConfig: CppDebugConfiguration = { ...defaultTemplateConfig }; // Copy enumerables and properties
newConfig.existing = false;
newConfig.name = configPrefix + compilerName + " " + this.buildAndDebugActiveFileStr();
newConfig.name = `${configPrefix}${compilerName} [Debugger: ${newConfig.type}] ${this.buildAndDebugActiveFileStr()}`;
newConfig.preLaunchTask = task.name;
if (newConfig.type === DebuggerType.cppdbg) {
newConfig.externalConsole = false;
} else {
newConfig.console = "integratedTerminal";
switch (newConfig.type) {
case DebuggerType.cppdbg:
newConfig.externalConsole = false;
break;
case DebuggerType.cppvsdbg:
newConfig.console = "integratedTerminal";
break;
}
// Extract the .exe path from the defined task.
const definedExePath: string | undefined = util.findExePathInArgs(task.definition.args);
newConfig.program = definedExePath ? definedExePath : util.defaultExePath();
@@ -473,28 +528,27 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
newConfig.type = DebuggerType.cppvsdbg;
return newConfig;
} else {
debuggerName = "gdb";
}
if (isWindows) {
debuggerName = debuggerName.endsWith(".exe") ? debuggerName : (debuggerName + ".exe");
debuggerName = executableName("gdb");
}
const compilerDirname: string = path.dirname(compilerPath);
const debuggerPath: string = path.join(compilerDirname, debuggerName);
// Check if debuggerPath exists.
if (await util.checkFileExists(debuggerPath)) {
newConfig.miDebuggerPath = debuggerPath;
} else if (await util.whichAsync(debuggerName) !== undefined) {
// Check if debuggerName exists on $PATH
newConfig.miDebuggerPath = debuggerName;
} else {
// Try the usr path for non-Windows platforms.
const usrDebuggerPath: string = path.join("/usr", "bin", debuggerName);
if (!isWindows && await util.checkFileExists(usrDebuggerPath)) {
newConfig.miDebuggerPath = usrDebuggerPath;
if (newConfig.type === DebuggerType.cppdbg) {
// Check if debuggerPath exists.
if (await util.checkFileExists(debuggerPath)) {
newConfig.miDebuggerPath = debuggerPath;
} else if (await util.whichAsync(debuggerName) !== undefined) {
// Check if debuggerName exists on $PATH
newConfig.miDebuggerPath = debuggerName;
} else {
logger.getOutputChannelLogger().appendLine(localize('debugger.path.not.exists', "Unable to find the {0} debugger. The debug configuration for {1} is ignored.", `\"${debuggerName}\"`, compilerName));
return undefined;
// Try the usr path for non-Windows platforms.
const usrDebuggerPath: string = path.join("/usr", "bin", debuggerName);
if (!isWindows && await util.checkFileExists(usrDebuggerPath)) {
newConfig.miDebuggerPath = usrDebuggerPath;
} else {
logger.getOutputChannelLogger().appendLine(localize('debugger.path.not.exists', "Unable to find the {0} debugger. The debug configuration for {1} is ignored.", `\"${debuggerName}\"`, compilerName));
return undefined;
}
}
}
}
@@ -890,11 +944,12 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
await cppBuildTaskProvider.writeBuildTask(selectedConfig.preLaunchTask);
}
// Remove the extra properties that are not a part of the DebugConfiguration, as these properties will be written in launch.json.
selectedConfig.detail = undefined;
selectedConfig.taskStatus = undefined;
selectedConfig.isDefault = undefined;
selectedConfig.source = undefined;
selectedConfig.debuggerEvent = undefined;
// Use 'delete' instead of 'undefined' to remove the property from the object (undefined properties still exist!).
delete selectedConfig.detail;
delete selectedConfig.taskStatus;
delete selectedConfig.isDefault;
delete selectedConfig.source;
delete selectedConfig.debuggerEvent;
// Write debug configuration in launch.json file.
await this.writeDebugConfig(selectedConfig, isExistingConfig, folder);
Telemetry.logDebuggerEvent(DebuggerEvent.addConfigGear, { "configSource": ConfigSource.workspaceFolder, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "true" });
@@ -934,10 +989,19 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
}
// Get debug configurations for all debugger types.
let configs: CppDebugConfiguration[] = await this.provideDebugConfigurationsForType(DebuggerType.cppdbg, folder);
if (os.platform() === 'win32') {
configs = configs.concat(await this.provideDebugConfigurationsForType(DebuggerType.cppvsdbg, folder));
let configs: CppDebugConfiguration[] = [];
if (isWindows) {
// When on Windows - prefer cppvsdbg.
configs.push(...await this.provideDebugConfigurationsForType(DebuggerType.cppvsdbg, folder));
}
if (!isWindows && await findLldbDap()) {
// Only include lldb-dap tasks if it's not Windows and lldb-dap is actually available.
configs.push(...await this.provideDebugConfigurationsForType(DebuggerType.cpplldb, folder));
}
configs.push(...await this.provideDebugConfigurationsForType(DebuggerType.cppdbg, folder));
if (onlyWorkspaceFolder) {
configs = configs.filter(item => !item.configSource || item.configSource === ConfigSource.workspaceFolder);
}
@@ -1065,7 +1129,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
return false;
}
let scpResult: util.ProcessReturnType;
let scpResult: ProcessReturnType;
if (isScp) {
scpResult = await scp(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken);
} else {
@@ -1086,7 +1150,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts;
const localForwards: util.ISshLocalForwardInfo[] = step.host.localForwards;
const continueOn: string = step.continueOn;
const sshResult: util.ProcessReturnType = await ssh(host, step.command, config.sshPath, jumpHosts, localForwards, continueOn, cancellationToken);
const sshResult: ProcessReturnType = await ssh(host, step.command, config.sshPath, jumpHosts, localForwards, continueOn, cancellationToken);
if (!sshResult.succeeded || cancellationToken?.isCancellationRequested) {
return false;
}
@@ -1097,7 +1161,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
void logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.shell', '"command" is required for shell steps.'));
return false;
}
const taskResult: util.ProcessReturnType = await util.spawnChildProcess(step.command, undefined, step.continueOn);
const taskResult: ProcessReturnType = await spawnChildProcess(step.command, undefined, step.continueOn);
if (!taskResult.succeeded || cancellationToken?.isCancellationRequested) {
void logger.getOutputChannelLogger().showErrorMessage(taskResult.output);
return false;
@@ -1113,13 +1177,26 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
}
}
export interface IConfigurationAssetProvider {
getInitialConfigurations(debuggerType: DebuggerType): any;
getConfigurationSnippets(): vscode.CompletionItem[];
}
export class ConfigurationAssetProvider {
configurations: Configuration[] = [];
export class ConfigurationAssetProviderFactory {
public static getConfigurationProvider(): IConfigurationAssetProvider {
protected constructor() {
}
public getInitialConfigurations(debuggerType: DebuggerType): any[] {
return this.configurations.map(configuration => configuration.GetLaunchConfiguration())
.filter(snippet => snippet.debuggerType === debuggerType && snippet.isInitialConfiguration)
.map(snippet => snippet.body);
}
public getConfigurationSnippets(): vscode.CompletionItem[] {
return this.configurations.map(configuration => [
convertConfigurationSnippetToCompletionItem(configuration.GetLaunchConfiguration()),
convertConfigurationSnippetToCompletionItem(configuration.GetAttachConfiguration())]
).flat();
}
static getConfigurationAssetProvider(): ConfigurationAssetProvider {
switch (os.platform()) {
case 'win32':
return new WindowsConfigurationProvider();
@@ -1128,104 +1205,79 @@ export class ConfigurationAssetProviderFactory {
case 'linux':
return new LinuxConfigurationProvider();
default:
throw new Error(localize("unexpected.os", "Unexpected OS type"));
return new ConfigurationAssetProvider(); // At least this will return empty results if the platform is not recognized.
}
}
}
abstract class DefaultConfigurationProvider implements IConfigurationAssetProvider {
configurations: IConfiguration[] = [];
public getInitialConfigurations(debuggerType: DebuggerType): any {
const configurationSnippet: IConfigurationSnippet[] = [];
// Only launch configurations are initial configurations
this.configurations.forEach(configuration => {
configurationSnippet.push(configuration.GetLaunchConfiguration());
});
const initialConfigurations: any = configurationSnippet.filter(snippet => snippet.debuggerType === debuggerType && snippet.isInitialConfiguration)
.map(snippet => JSON.parse(snippet.bodyText));
// If configurations is empty, then it will only have an empty configurations array in launch.json. Users can still add snippets.
return initialConfigurations;
}
public getConfigurationSnippets(): vscode.CompletionItem[] {
const completionItems: vscode.CompletionItem[] = [];
this.configurations.forEach(configuration => {
completionItems.push(convertConfigurationSnippetToCompletionItem(configuration.GetLaunchConfiguration()));
completionItems.push(convertConfigurationSnippetToCompletionItem(configuration.GetAttachConfiguration()));
});
return completionItems;
}
}
class WindowsConfigurationProvider extends DefaultConfigurationProvider {
class WindowsConfigurationProvider extends ConfigurationAssetProvider {
private executable: string = "a.exe";
private pipeProgram: string = "<" + localize("path.to.pipe.program", "full path to pipe program such as {0}", "plink.exe").replace(/"/g, '') + ">";
private MIMode: string = 'gdb';
private setupCommandsBlock: string = `"setupCommands": [
{
"description": "${localize("enable.pretty.printing", "Enable pretty-printing for {0}", "gdb").replace(/"/g, '')}",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "${localize("enable.intel.disassembly.flavor", "Set Disassembly Flavor to {0}", "Intel").replace(/"/g, '')}",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]`;
private additionalProperties = {
setupCommands: [
{
description: localize("enable.pretty.printing", "Enable pretty-printing for {0}", "gdb").replace(/"/g, ''),
text: "-enable-pretty-printing",
ignoreFailures: true
},
{
description: localize("enable.intel.disassembly.flavor", "Set Disassembly Flavor to {0}", "Intel").replace(/"/g, ''),
text: "-gdb-set disassembly-flavor intel",
ignoreFailures: true
}
]
};
constructor() {
super();
this.configurations = [
new MIConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock),
new PipeTransportConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock),
new WindowsConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock),
new WSLConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock)
new WindowsConfigurations(this.executable, this.additionalProperties),
new MIConfigurations('gdb', this.executable, this.additionalProperties),
new PipeTransportConfigurations('gdb', this.executable, this.pipeProgram, this.additionalProperties),
new WSLConfigurations('gdb', this.executable, this.additionalProperties),
new LldbDapConfigurations(this.executable)
];
}
}
class OSXConfigurationProvider extends DefaultConfigurationProvider {
private MIMode: string = 'lldb';
class OSXConfigurationProvider extends ConfigurationAssetProvider {
private executable: string = "a.out";
constructor() {
super();
this.configurations = [
new LldbDapConfigurations(this.executable),
new MIConfigurations('lldb', this.executable)
];
}
}
class LinuxConfigurationProvider extends ConfigurationAssetProvider {
private additionalProperties = {
setupCommands: [
{
description: localize("enable.pretty.printing", "Enable pretty-printing for {0}", "gdb").replace(/"/g, ''),
text: "-enable-pretty-printing",
ignoreFailures: true
},
{
description: localize("enable.intel.disassembly.flavor", "Set Disassembly Flavor to {0}", "Intel").replace(/"/g, ''),
text: "-gdb-set disassembly-flavor intel",
ignoreFailures: true
}
]
};
private executable: string = "a.out";
private pipeProgram: string = "/usr/bin/ssh";
constructor() {
super();
this.configurations = [
new MIConfigurations(this.MIMode, this.executable, this.pipeProgram)
];
}
}
class LinuxConfigurationProvider extends DefaultConfigurationProvider {
private MIMode: string = 'gdb';
private setupCommandsBlock: string = `"setupCommands": [
{
"description": "${localize("enable.pretty.printing", "Enable pretty-printing for {0}", "gdb").replace(/"/g, '')}",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "${localize("enable.intel.disassembly.flavor", "Set Disassembly Flavor to {0}", "Intel").replace(/"/g, '')}",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]`;
private executable: string = "a.out";
private pipeProgram: string = "/usr/bin/ssh";
constructor() {
super();
this.configurations = [
new MIConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock),
new PipeTransportConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock)
new MIConfigurations('gdb', this.executable, this.additionalProperties),
new PipeTransportConfigurations('gdb', this.executable, this.pipeProgram, this.additionalProperties),
new LldbDapConfigurations(this.executable)
];
}
}
@@ -1233,17 +1285,15 @@ class LinuxConfigurationProvider extends DefaultConfigurationProvider {
function convertConfigurationSnippetToCompletionItem(snippet: IConfigurationSnippet): vscode.CompletionItem {
const item: vscode.CompletionItem = new vscode.CompletionItem(snippet.label, vscode.CompletionItemKind.Module);
item.insertText = snippet.bodyText;
item.insertText = JSON.stringify(snippet.body, null, '\t');
return item;
}
export class ConfigurationSnippetProvider implements vscode.CompletionItemProvider {
private provider: IConfigurationAssetProvider;
private snippets: vscode.CompletionItem[];
constructor(provider: IConfigurationAssetProvider) {
this.provider = provider;
constructor(private provider: ConfigurationAssetProvider) {
this.snippets = this.provider.getConfigurationSnippets();
}
public resolveCompletionItem(item: vscode.CompletionItem, _token: vscode.CancellationToken): Thenable<vscode.CompletionItem> {
+154 -153
View File
@@ -3,16 +3,16 @@
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as os from 'os';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { configPrefix } from '../LanguageServer/extension';
import { isWindows } from '../constants';
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
export function isDebugLaunchStr(str: string): boolean {
return str.startsWith("(gdb) ") || str.startsWith("(lldb) ") || str.startsWith("(Windows) ");
return str.startsWith("(gdb) ") || str.startsWith("(lldb") || str.startsWith("(Windows) ");
}
export interface ConfigMenu extends vscode.QuickPickItem {
@@ -22,6 +22,7 @@ export interface ConfigMenu extends vscode.QuickPickItem {
export enum DebuggerType {
cppvsdbg = "cppvsdbg",
cppdbg = "cppdbg",
cpplldb = "cpplldb",
all = "all"
}
@@ -70,147 +71,121 @@ export interface CppDebugConfiguration extends vscode.DebugConfiguration {
export interface IConfigurationSnippet {
label: string;
description: string;
bodyText: string;
body: Record<string, any>;
// Internal
isInitialConfiguration?: boolean;
debuggerType: DebuggerType;
}
export function indentJsonString(json: string, numTabs: number = 1): string {
return json.split('\n').map(line => '\t'.repeat(numTabs) + line).join('\n').trim();
function createLaunchBlock(name: string, type: string, executable: string): Record<string, any> {
return {
name: name,
type: type,
request: "launch",
program: localize("enter.program.name", "enter program name, for example {0}", "${workspaceFolder}" + "/" + executable).replace(/"/g, ''),
args: [],
stopAtEntry: false,
cwd: "${fileDirname}",
environment: [],
externalConsole: type === DebuggerType.cppdbg ? false : type === DebuggerType.cpplldb ? true : undefined,
console: type === DebuggerType.cppvsdbg ? "externalTerminal" : undefined
};
}
function formatString(format: string, args: string[]): string {
args.forEach((arg: string, index: number) => {
format = format.replace("{" + index + "}", arg);
});
return format;
function createAttachBlock(name: string, type: string, executable: string): Record<string, any> {
return {
name: name,
type: type,
request: "attach",
program: type === DebuggerType.cppdbg ? localize("enter.program.name", "enter program name, for example {0}", "${workspaceFolder}" + "/" + executable).replace(/"/g, '') : undefined
};
}
function createLaunchString(name: string, type: string, executable: string): string {
return `"name": "${name}",
"type": "${type}",
"request": "launch",
"program": "${localize("enter.program.name", "enter program name, for example {0}", "$\{workspaceFolder\}" + "/" + executable).replace(/"/g, '')}",
"args": [],
"stopAtEntry": false,
"cwd": "$\{fileDirname\}",
"environment": [],
${ type === "cppdbg" ? `"externalConsole": false` : `"console": "externalTerminal"` }
`;
function createRemoteAttachBlock(name: string, type: string, executable: string): Record<string, any> {
return {
name: name,
type: type,
request: "attach",
program: localize("enter.program.name", "enter program name, for example {0}", "${workspaceFolder}" + "/" + executable).replace(/"/g, ''),
processId: "${command:pickRemoteProcess}"
};
}
function createAttachString(name: string, type: string, executable: string): string {
return formatString(`
"name": "${name}",
"type": "${type}",
"request": "attach",{0}
`, [type === "cppdbg" ? `${os.EOL}"program": "${localize("enter.program.name", "enter program name, for example {0}", "$\{workspaceFolder\}" + "/" + executable).replace(/"/g, '')}",` : ""]);
function createPipeTransportBlock(pipeProgram: string, debuggerProgram: string, pipeArgs: string[] = []): Record<string, any> {
return {
pipeTransport: {
debuggerPath: `/usr/bin/${debuggerProgram}`,
pipeProgram: pipeProgram,
pipeArgs: pipeArgs,
pipeCwd: ""
}
};
}
function createRemoteAttachString(name: string, type: string, executable: string): string {
return `
"name": "${name}",
"type": "${type}",
"request": "attach",
"program": "${localize("enter.program.name", "enter program name, for example {0}", "$\{workspaceFolder\}" + "/" + executable).replace(/"/g, '')}",
"processId": "$\{command:pickRemoteProcess\}"
`;
}
function createPipeTransportString(pipeProgram: string, debuggerProgram: string, pipeArgs: string[] = []): string {
return `
"pipeTransport": {
\t"debuggerPath": "/usr/bin/${debuggerProgram}",
\t"pipeProgram": "${pipeProgram}",
\t"pipeArgs": ${JSON.stringify(pipeArgs)},
\t"pipeCwd": ""
}`;
}
export interface IConfiguration {
GetLaunchConfiguration(): IConfigurationSnippet;
GetAttachConfiguration(): IConfigurationSnippet;
}
abstract class Configuration implements IConfiguration {
public executable: string;
public pipeProgram: string;
public MIMode: string;
public additionalProperties: string;
public miDebugger = "cppdbg";
public windowsDebugger = "cppvsdbg";
constructor(MIMode: string, executable: string, pipeProgram: string, additionalProperties: string = "") {
this.MIMode = MIMode;
this.executable = executable;
this.pipeProgram = pipeProgram;
this.additionalProperties = additionalProperties;
}
export abstract class Configuration {
abstract GetLaunchConfiguration(): IConfigurationSnippet;
abstract GetAttachConfiguration(): IConfigurationSnippet;
}
/** Creates Configurations for an MI debugger */
export class MIConfigurations extends Configuration {
constructor(public MIMode: string, public executable: string, public additionalProperties: Record<string, any> = {}) {
super();
}
public GetLaunchConfiguration(): IConfigurationSnippet {
const name: string = `(${this.MIMode}) ${localize("launch.string", "Launch").replace(/"/g, '')}`;
const body: string = formatString(`{
\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)}` : ""]);
return {
"label": configPrefix + name,
"description": localize("launch.with", "Launch with {0}.", this.MIMode).replace(/"/g, ''),
"bodyText": body.trim(),
"isInitialConfiguration": true,
"debuggerType": DebuggerType.cppdbg
label: configPrefix + name,
description: localize("launch.with", "Launch with {0}.", this.MIMode).replace(/"/g, ''),
body: {
...createLaunchBlock(name, DebuggerType.cppdbg, this.executable),
MIMode: this.MIMode,
miDebuggerPath: isWindows ? `<${localize("path.to.gdb", "path to gdb").replace(/"/g, '')}>` : undefined,
...this.additionalProperties
},
isInitialConfiguration: true,
debuggerType: DebuggerType.cppdbg
};
}
public GetAttachConfiguration(): IConfigurationSnippet {
const name: string = `(${this.MIMode}) ${localize("attach.string", "Attach").replace(/"/g, '')}`;
const body: string = formatString(`{
\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)}` : ""]);
return {
"label": configPrefix + name,
"description": localize("attach.with", "Attach with {0}.", this.MIMode).replace(/"/g, ''),
"bodyText": body.trim(),
"debuggerType": DebuggerType.cppdbg
label: configPrefix + name,
description: localize("attach.with", "Attach with {0}.", this.MIMode).replace(/"/g, ''),
body: {
...createAttachBlock(name, DebuggerType.cppdbg, this.executable),
MIMode: this.MIMode,
miDebuggerPath: isWindows ? `<${localize("path.to.gdb", "path to gdb").replace(/"/g, '')}>` : undefined,
...this.additionalProperties
},
debuggerType: DebuggerType.cppdbg
};
}
}
export class PipeTransportConfigurations extends Configuration {
constructor(public MIMode: string, public executable: string, public pipeProgram: string, public additionalProperties: Record<string, any> = {}) {
super();
}
public GetLaunchConfiguration(): IConfigurationSnippet {
const name: string = `(${this.MIMode}) ${localize("pipe.launch", "Pipe Launch").replace(/"/g, '')}`;
const body: string = formatString(`
{
\t${indentJsonString(createLaunchString(name, this.miDebugger, this.executable))},
\t${indentJsonString(createPipeTransportString(this.pipeProgram, this.MIMode))},
\t"MIMode": "${this.MIMode}"{0}
}`, [this.additionalProperties ? `,${os.EOL}\t${indentJsonString(this.additionalProperties)}` : ""]);
return {
"label": configPrefix + name,
"description": localize("pipe.launch.with", "Pipe Launch with {0}.", this.MIMode).replace(/"/g, ''),
"bodyText": body.trim(),
"debuggerType": DebuggerType.cppdbg
label: configPrefix + name,
description: localize("pipe.launch.with", "Pipe Launch with {0}.", this.MIMode).replace(/"/g, ''),
body: {
...createLaunchBlock(name, DebuggerType.cppdbg, this.executable),
...createPipeTransportBlock(this.pipeProgram, this.MIMode),
MIMode: this.MIMode,
...this.additionalProperties
},
debuggerType: DebuggerType.cppdbg
};
}
@@ -218,38 +193,35 @@ export class PipeTransportConfigurations extends Configuration {
public GetAttachConfiguration(): IConfigurationSnippet {
const name: string = `(${this.MIMode}) ${localize("pipe.attach", "Pipe Attach").replace(/"/g, '')}`;
const body: string = formatString(`
{
\t${indentJsonString(createRemoteAttachString(name, this.miDebugger, this.executable))},
\t${indentJsonString(createPipeTransportString(this.pipeProgram, this.MIMode))},
\t"MIMode": "${this.MIMode}"{0}
}`, [this.additionalProperties ? `,${os.EOL}\t${indentJsonString(this.additionalProperties)}` : ""]);
return {
"label": configPrefix + name,
"description": localize("pipe.attach.with", "Pipe Attach with {0}.", this.MIMode).replace(/"/g, ''),
"bodyText": body.trim(),
"debuggerType": DebuggerType.cppdbg
label: configPrefix + name,
description: localize("pipe.attach.with", "Pipe Attach with {0}.", this.MIMode).replace(/"/g, ''),
body: {
...createRemoteAttachBlock(name, DebuggerType.cppdbg, this.executable),
...createPipeTransportBlock(this.pipeProgram, this.MIMode),
MIMode: this.MIMode,
...this.additionalProperties
},
debuggerType: DebuggerType.cppdbg
};
}
}
export class WindowsConfigurations extends Configuration {
constructor(public executable: string, public additionalProperties: Record<string, any> = {}) {
super();
}
public GetLaunchConfiguration(): IConfigurationSnippet {
const name: string = `(Windows) ${localize("launch.string", "Launch").replace(/"/g, '')}`;
const body: string = `
{
\t${indentJsonString(createLaunchString(name, this.windowsDebugger, this.executable))}
}`;
return {
"label": configPrefix + name,
"description": localize("launch.with.vs.debugger", "Launch with the Visual Studio C/C++ debugger.").replace(/"/g, ''),
"bodyText": body.trim(),
"isInitialConfiguration": true,
"debuggerType": DebuggerType.cppvsdbg
label: configPrefix + name,
description: localize("launch.with.vs.debugger", "Launch with the Visual Studio C/C++ debugger.").replace(/"/g, ''),
body: createLaunchBlock(name, DebuggerType.cppvsdbg, this.executable),
isInitialConfiguration: true,
debuggerType: DebuggerType.cppvsdbg
};
}
@@ -257,56 +229,85 @@ export class WindowsConfigurations extends Configuration {
public GetAttachConfiguration(): IConfigurationSnippet {
const name: string = `(Windows) ${localize("attach.string", "Attach").replace(/"/g, '')}`;
const body: string = `
{
\t${indentJsonString(createAttachString(name, this.windowsDebugger, this.executable))}
}`;
return {
"label": configPrefix + name,
"description": localize("attach.with.vs.debugger", "Attach to a process with the Visual Studio C/C++ debugger.").replace(/"/g, ''),
"bodyText": body.trim(),
"debuggerType": DebuggerType.cppvsdbg
label: configPrefix + name,
description: localize("attach.with.vs.debugger", "Attach to a process with the Visual Studio C/C++ debugger.").replace(/"/g, ''),
body: createAttachBlock(name, DebuggerType.cppvsdbg, this.executable),
debuggerType: DebuggerType.cppvsdbg
};
}
}
export class WSLConfigurations extends Configuration {
constructor(public MIMode: string, public executable: string, public additionalProperties: Record<string, any> = {}) {
super();
}
// Detects if the current VSCode is 32-bit and uses the correct bash.exe
public bashPipeProgram = process.arch === 'ia32' ? "${env:windir}\\\\sysnative\\\\bash.exe" : "${env:windir}\\\\system32\\\\bash.exe";
public GetLaunchConfiguration(): IConfigurationSnippet {
const name: string = `(${this.MIMode}) ${localize("bash.on.windows.launch", "Bash on Windows Launch").replace(/"/g, '')}`;
const body: string = formatString(`
{
\t${indentJsonString(createLaunchString(name, this.miDebugger, this.executable))},
\t${indentJsonString(createPipeTransportString(this.bashPipeProgram, this.MIMode, ["-c"]))}{0}
}`, [this.additionalProperties ? `,${os.EOL}\t${indentJsonString(this.additionalProperties)}` : ""]);
return {
"label": configPrefix + name,
"description": localize("launch.bash.windows", "Launch in Bash on Windows using {0}.", this.MIMode).replace(/"/g, ''),
"bodyText": body.trim(),
"debuggerType": DebuggerType.cppdbg
label: configPrefix + name,
description: localize("launch.bash.windows", "Launch in Bash on Windows using {0}.", this.MIMode).replace(/"/g, ''),
body: {
...createLaunchBlock(name, DebuggerType.cppdbg, this.executable),
...createPipeTransportBlock(this.bashPipeProgram, this.MIMode, ["-c"]),
...this.additionalProperties
},
debuggerType: DebuggerType.cppdbg
};
}
public GetAttachConfiguration(): IConfigurationSnippet {
const name: string = `(${this.MIMode}) ${localize("bash.on.windows.attach", "Bash on Windows Attach").replace(/"/g, '')}`;
const body: string = formatString(`
{
\t${indentJsonString(createRemoteAttachString(name, this.miDebugger, this.executable))},
\t${indentJsonString(createPipeTransportString(this.bashPipeProgram, this.MIMode, ["-c"]))}{0}
}`, [this.additionalProperties ? `,${os.EOL}\t${indentJsonString(this.additionalProperties)}` : ""]);
return {
"label": configPrefix + name,
"description": localize("remote.attach.bash.windows", "Attach to a remote process running in Bash on Windows using {0}.", this.MIMode).replace(/"/g, ''),
"bodyText": body.trim(),
"debuggerType": DebuggerType.cppdbg
label: configPrefix + name,
description: localize("remote.attach.bash.windows", "Attach to a remote process running in Bash on Windows using {0}.", this.MIMode).replace(/"/g, ''),
body: {
...createRemoteAttachBlock(name, DebuggerType.cppdbg, this.executable),
...createPipeTransportBlock(this.bashPipeProgram, this.MIMode, ["-c"]),
...this.additionalProperties
},
debuggerType: DebuggerType.cppdbg
};
}
}
/** Creates Configurations for an LLDB-DAP debugger */
export class LldbDapConfigurations extends Configuration {
constructor(public executable: string, public additionalProperties: Record<string, any> = {}) {
super();
}
public GetLaunchConfiguration(): IConfigurationSnippet {
const name: string = `(lldb-dap) ${localize("launch.string", "Launch").replace(/"/g, '')}`;
return {
label: configPrefix + name,
description: localize("launch.with", "Launch with {0}.", "LLDB-DAP").replace(/"/g, ''),
body: {
...createLaunchBlock(name, DebuggerType.cpplldb, this.executable),
...this.additionalProperties
},
isInitialConfiguration: true,
debuggerType: DebuggerType.cpplldb
};
}
public GetAttachConfiguration(): IConfigurationSnippet {
const name: string = `(lldb-dap) ${localize("attach.string", "Attach").replace(/"/g, '')}`;
return {
label: configPrefix + name,
description: localize("attach.with", "Attach with {0}.", "LLDB-DAP").replace(/"/g, ''),
body: {
...createAttachBlock(name, DebuggerType.cpplldb, this.executable),
...this.additionalProperties
},
debuggerType: DebuggerType.cpplldb
};
}
}
@@ -7,6 +7,8 @@ import * as os from 'os';
import * as path from 'path';
import * as vscode from "vscode";
import * as nls from 'vscode-nls';
import { DebuggerType } from './configurations';
import { findLldbDap, isValidLldbDap } from './lldb-dap';
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
@@ -40,7 +42,7 @@ export class CppvsdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterD
async createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor | null> {
if (os.platform() !== 'win32') {
void vscode.window.showErrorMessage(localize("debugger.not.available", "Debugger type '{0}' is not available for non-Windows machines.", "cppvsdbg"));
void vscode.window.showErrorMessage(localize("debugger.not.available", "Debugger type '{0}' is not available for non-Windows machines.", DebuggerType.cppvsdbg));
return null;
} else {
return new vscode.DebugAdapterExecutable(
@@ -50,3 +52,43 @@ export class CppvsdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterD
}
}
}
/** Generates the command line for the LLDB-DAP debugger */
export class CpplldbDebugAdapterDescriptorFactory extends AbstractDebugAdapterDescriptorFactory {
async createDebugAdapterDescriptor(session: vscode.DebugSession, executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor | null> {
// The adapter path can be specified in the launch.json entry.
let adapter: string | undefined = session.configuration.debuggerPath || executable?.command;
if (adapter) {
// Verify that the path is actually valid.
if (!await isValidLldbDap(adapter)) {
adapter = await findLldbDap();
if (adapter) {
void vscode.window.showErrorMessage(localize("debugger.path.not.available.falling.back", "The specified LLDB-DAP debuggerPath '{0}' is not valid, falling back to {1}.", session.configuration.debuggerPath, adapter));
} else {
void vscode.window.showErrorMessage(localize("debugger.path.not.available", "The specified LLDB-DAP debuggerPath '{0}' is not valid and no fallback was found.", session.configuration.debuggerPath));
return null;
}
}
}
if (!adapter) {
adapter = await findLldbDap();
}
if (!adapter) {
void vscode.window.showErrorMessage(localize("lldbdap.debugger.not.available", "No LLDB-DAP debugger found. Please add it to the path, or set the debuggerPath property in the launch.json file."));
return null;
}
// Prepare the command to run the lldb-dap executable.
const debuggerArgs = session.configuration.debuggerArgs || [];
// Future: add support for pipeTransport (so that the lldb-dap executable can be run on a remote machine or wsl).
// Future: add support for --server mode (so that the lldb-dap executable can be run in server mode).
// Prepare the command to run the lldb-dap executable.
return new vscode.DebugAdapterExecutable(adapter, [...debuggerArgs]);
}
}
+9 -6
View File
@@ -14,13 +14,13 @@ import { SshTargetsProvider, getActiveSshTarget, initializeSshTargets, selectSsh
import { TargetLeafNode, setActiveSshTarget } from '../SSH/TargetsView/targetNodes';
import { sshCommandToConfig } from '../SSH/sshCommandToConfig';
import { getSshConfiguration, getSshConfigurationFiles, parseFailures, writeSshConfiguration } from '../SSH/sshHosts';
import { pathAccessible } from '../common';
import { pathAccessible } from '../common-remote-safe';
import { instrument } from '../instrumentation';
import { getSshChannel } from '../logger';
import { AttachItemsProvider, AttachPicker, RemoteAttachPicker } from './attachToProcess';
import { ConfigurationAssetProviderFactory, ConfigurationSnippetProvider, DebugConfigurationProvider, IConfigurationAssetProvider } from './configurationProvider';
import { ConfigurationAssetProvider, ConfigurationSnippetProvider, DebugConfigurationProvider } from './configurationProvider';
import { DebuggerType } from './configurations';
import { CppdbgDebugAdapterDescriptorFactory, CppvsdbgDebugAdapterDescriptorFactory } from './debugAdapterDescriptorFactory';
import { CppdbgDebugAdapterDescriptorFactory, CpplldbDebugAdapterDescriptorFactory, CppvsdbgDebugAdapterDescriptorFactory } from './debugAdapterDescriptorFactory';
import { NativeAttachItemsProviderFactory } from './nativeAttach';
// The extension deactivate method is asynchronous, so we handle the disposables ourselves instead of using extensionContext.subscriptions.
@@ -40,18 +40,20 @@ export async function initialize(context: vscode.ExtensionContext): Promise<void
disposables.push(vscode.commands.registerCommand('extension.pickRemoteNativeProcess', (any) => remoteAttacher.ShowAttachEntries(any)));
// Activate ConfigurationProvider
const assetProvider: IConfigurationAssetProvider = ConfigurationAssetProviderFactory.getConfigurationProvider();
const assetProvider = ConfigurationAssetProvider.getConfigurationAssetProvider();
// Register DebugConfigurationProviders for "Run and Debug" in Debug Panel.
// On Windows platforms, the cppvsdbg debugger will also be registered for initial configurations.
let cppVsDebugProvider: DebugConfigurationProvider | null = null;
if (os.platform() === 'win32') {
cppVsDebugProvider = new DebugConfigurationProvider(assetProvider, DebuggerType.cppvsdbg);
const cppVsDebugProvider = new DebugConfigurationProvider(assetProvider, DebuggerType.cppvsdbg);
disposables.push(vscode.debug.registerDebugConfigurationProvider(DebuggerType.cppvsdbg, instrument(cppVsDebugProvider), vscode.DebugConfigurationProviderTriggerKind.Dynamic));
}
const cppDebugProvider: DebugConfigurationProvider = new DebugConfigurationProvider(assetProvider, DebuggerType.cppdbg);
disposables.push(vscode.debug.registerDebugConfigurationProvider(DebuggerType.cppdbg, instrument(cppDebugProvider), vscode.DebugConfigurationProviderTriggerKind.Dynamic));
const cpplldbDebugProvider: DebugConfigurationProvider = new DebugConfigurationProvider(assetProvider, DebuggerType.cpplldb);
disposables.push(vscode.debug.registerDebugConfigurationProvider(DebuggerType.cpplldb, instrument(cpplldbDebugProvider), vscode.DebugConfigurationProviderTriggerKind.Dynamic));
// Register DebugConfigurationProviders for "Run and Debug" play button.
const debugProvider: DebugConfigurationProvider = new DebugConfigurationProvider(assetProvider, DebuggerType.all);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -81,6 +83,7 @@ export async function initialize(context: vscode.ExtensionContext): Promise<void
// Register Debug Adapters
disposables.push(vscode.debug.registerDebugAdapterDescriptorFactory(DebuggerType.cppvsdbg, new CppvsdbgDebugAdapterDescriptorFactory(context)));
disposables.push(vscode.debug.registerDebugAdapterDescriptorFactory(DebuggerType.cppdbg, new CppdbgDebugAdapterDescriptorFactory(context)));
disposables.push(vscode.debug.registerDebugAdapterDescriptorFactory(DebuggerType.cpplldb, new CpplldbDebugAdapterDescriptorFactory(context)));
// SSH Targets View
await initializeSshTargets();
+277
View File
@@ -0,0 +1,277 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { isMainThread, parentPort } from 'node:worker_threads';
import { appendLineAtLevel, executableName, findInPath, initialize, isExecutable, localize, searchFolders, spawnChildProcess } from '../common-remote-safe';
import { isLinux, isMacOS, isWindows } from '../constants';
import { TroubleshootingLldbDap } from '../links';
import { ManualPromise } from '../Utility/Async/manualPromise';
import { RemoteConnection, startRemoting } from '../Utility/Remoting/snare';
let remote: RemoteConnection | undefined;
/**
* Base interface for debug configurations.
* Represents a configuration used by the VS Code debugger.
*/
export interface DebugConfiguration {
/**
* The type of the debug session.
*/
type: string;
/**
* The name of the debug session.
*/
name: string;
/**
* The request type of the debug session.
*/
request: string;
/**
* Additional debug type specific properties.
*/
[key: string]: any;
}
/**
* C++ specific debug configuration interface.
* Extends the base debug configuration with C++ specific properties.
*/
export interface CppDebugConfiguration extends DebugConfiguration {
/** Detailed description of the configuration. */
detail?: string;
/** Status of the associated build task. */
taskStatus?: any;
/**
* Indicates whether this is the default debug configuration.
* The debug configuration is considered as default if the prelaunch task is set as default.
*/
isDefault?: boolean;
/** Source of the configuration (e.g., auto-generated, user-defined). */
configSource?: any;
/** Debugger-specific event information. */
debuggerEvent?: any;
/** Type of debugging to perform. */
debugType?: any;
/** Indicates whether this configuration represents an existing debug session. */
existing?: boolean;
}
/**
* Enum representing the supported debugger types in the C++ extension.
*/
export enum DebuggerType {
/** Microsoft Visual Studio Debugger */
cppvsdbg = "cppvsdbg",
/** GDB Debugger */
cppdbg = "cppdbg",
/** LLDB Debugger with Debug Adapter Protocol support */
cpplldb = "cpplldb",
/** Represents all debugger types */
all = "all"
}
/** Cached path to the lldb-dap executable once found. */
let lldbDapPath: string | undefined;
/** Cached path to the xcrun tool on macOS. */
let xcRunPath: string | undefined;
/**
* On macOS, calls xcrun to find a given xtools binary (usually they don't expose the binary to the PATH).
*
* @param filename The name of the binary to find (e.g., lldb-vscode or lldb-dap).
* @returns The path to the binary or undefined if it was not found.
*/
export async function xcRun(filename: string) {
if (isMacOS) {
try {
xcRunPath ??= await findInPath('xcrun');
if (xcRunPath) {
const xcRun = await spawnChildProcess(xcRunPath, ['--find', filename]);
if (xcRun.succeeded) {
const path = xcRun.output.trim();
if (await isValidLldbDap(path)) {
return path;
}
}
}
} catch {
// Ignore failure to run xcrun, or no results found.
}
}
return undefined;
}
/** Promise representing an ongoing search for the lldb-dap executable. */
let searching: ManualPromise<string | undefined> | undefined;
/**
* List of candidate file names and patterns to search for when looking for the lldb-dap executable.
* Includes both string literals and regular expressions to match version-specific binaries.
*/
const candidates = [
executableName('lldb-dap'),
/^lldb-dap-\d+$|^lldb-dap-\d+\.exe$/,
executableName('lldb-vscode'),
/^lldb-vscode-\d+$|^lldb-vscode-\d+\.exe$/
];
/**
* The search implementation to find the viable lldb-dap executable.
*
* We absolutely prefer an lldb-dap binary that is in the path.
* Failing that, we'll:
* - Try xcrun on macOS to see if xtools can give it to us
* - Try searching the secure well-known locations for a binary
*
* And if we can't find the actual lldb-dap binary, we'll try the same thing
* for 'lldb-vscode' (the old name for the DAP binary)
* and then 'lldb-dap-##' and 'lldb-vscode-##'. In practice, one might find a
* binary with the LLVM major version number in it.
*
* @returns The path to the lldb-dap executable or undefined if it was not found.
*/
async function searchForLldbDap() {
if (lldbDapPath) {
return lldbDapPath;
}
const start = Date.now();
// PATH binaries take priority.
for (const candidate of candidates) {
// First, search the environment PATH for the binary.
lldbDapPath = await findInPath(candidate, isValidLldbDap);
if (lldbDapPath) {
appendLineAtLevel(6, `Discovered lldb-dap binary at '${lldbDapPath}' ${Date.now() - start} msec`);
return lldbDapPath;
}
}
// Well-known locations next.
for (const candidate of candidates) {
if (isMacOS) {
// If that fails, use xcrun to find the path.
if (typeof candidate === 'string') {
lldbDapPath = await xcRun(candidate);
if (lldbDapPath) {
appendLineAtLevel(6, `Discovered lldb-dap binary for macOS at '${lldbDapPath}' ${Date.now() - start} msec`);
return lldbDapPath;
}
}
// If we got this far, it's not in the PATH or via xcrun - but worry not,
// we'll do a check of well known secured locations where it might be installed.
lldbDapPath = await searchFolders(['/Applications', '/opt/homebrew'], candidate, isValidLldbDap, 8);
if (lldbDapPath) {
appendLineAtLevel(6, `Discovered lldb-dap binary for macOS at '${lldbDapPath}' ${Date.now() - start} msec`);
return lldbDapPath;
}
}
if (isWindows) {
// If we got this far, it's not in the PATH - but worry not,
// we'll do a check of well known secured locations where it might be installed.
lldbDapPath = await searchFolders(['c:/Program Files/LLVM', 'c:/Program Files/', 'C:/program files (x86)/'], candidate, isValidLldbDap);
if (lldbDapPath) {
appendLineAtLevel(6, `Discovered lldb-dap binary for Windows at '${lldbDapPath}' ${Date.now() - start} msec`);
return lldbDapPath;
}
}
if (isLinux) {
// If we got this far, it's not in the PATH - but worry not,
// we'll do a check of well known secured locations where it might be installed.
lldbDapPath = await searchFolders(['/usr', '/opt'], candidate, isValidLldbDap, 8);
if (lldbDapPath) {
appendLineAtLevel(6, `Discovered lldb-dap binary for Linux at '${lldbDapPath}' ${Date.now() - start} msec`);
return lldbDapPath;
}
}
}
appendLineAtLevel(1, localize('lldb-dap.notfound', "Unable to find a working '{0}' adapter ({1} msec). See: {2}", 'lldb-dap', Date.now() - start, TroubleshootingLldbDap));
return lldbDapPath;
}
/**
* Calls searchForLldbDap, but only runs one search at a time. Subsequent calls return the current search.
*
* @returns The path to the lldb-dap executable or undefined if it was not found.
*/
export async function findLldbDapImpl() {
// If we already have a path, return it.
if (lldbDapPath) {
return lldbDapPath;
}
if (searching) {
return searching;
}
// Only run one search at a time, so if we are already searching, return that.
searching = new ManualPromise<string | undefined>();
searchForLldbDap().then((result: string | undefined): void => {
if (searching) {
searching.resolve(result);
searching = undefined;
}
}).catch(error => {
if (searching) {
searching.reject(error);
searching = undefined;
}
});
return searching;
}
/**
* Check if the given path is a valid lldb-dap executable (actually runs it to check).
*
* @param lldbDap The path to the lldb-dap executable.
* @returns True if the path is valid, false otherwise.
*/
export async function isValidLldbDap(lldbDap: string | undefined) {
if (lldbDap) {
if (await isExecutable(lldbDap)) {
const proc = await spawnChildProcess(lldbDap, ['--help'], undefined, true);
if (proc.succeeded && proc.output.includes('USAGE')) {
return true;
}
appendLineAtLevel(6, localize('lldb-dap.not.valid', "The lldb-dap binary at '{0}' does not appear to be functional. See: {1}", lldbDap, TroubleshootingLldbDap));
}
}
return false;
}
if (!isMainThread) {
// If this is loaded in a worker thread, we'll set up the remoting interface.
try {
/** This is the SNARE remote call interface dispatcher that the worker thread supports. */
remote = parentPort ? startRemoting(parentPort, {
// These are the functions that this worker exposes to the parent thread.
findLldbDapImpl
}) : undefined; // If we're not in a worker thread - then we don't really have a remote interface.
initialize(remote);
} catch (e) {
if (e instanceof Error) {
appendLineAtLevel(6, `Error in worker thread: ${e.message}`);
}
}
}
+112
View File
@@ -0,0 +1,112 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { resolve } from 'node:path';
import { isMainThread } from 'node:worker_threads';
import { appendLine, appendLineAtLevel, localize, log, note } from '../common-remote-safe';
import { isWindows } from '../constants';
import { RemoteConnection, startRemoting, startWorker } from '../Utility/Remoting/snare';
import { CppDebugConfiguration, DebuggerType } from './configurations';
import { findLldbDapImpl } from './lldb-dap-worker';
export { isValidLldbDap } from './lldb-dap-worker';
/** Translates the cpplldb configuration to the lldb-dap configuration.
* Note: this modifies the existing configuration object in place.
*
* @param config The cpplldb configuration to translate.
* @returns The translated lldb-dap configuration.
*/
export function translateToLldbDap(config: CppDebugConfiguration) {
// Adapt the cpplldb config to the lldb-dap config.
if (config.type !== DebuggerType.cpplldb) {
throw new Error(`Invalid config type ${config.type} for lldb-dap`);
}
// Translate environment to env.
if (config.environment) {
// 'config.environment' is an array of { "name":<string>, "value":<string> } objects.
// lldb-dap expects an object of { <name>:<value>, ... }.
const env: { [key: string]: string } = {};
for (const each of config.environment) {
if (typeof each.name === 'string' && typeof each.value === 'string') {
env[each.name] = each.value;
}
}
delete config.environment;
}
// Translate stopAtEntry to stopOnEntry.
if (config.stopAtEntry !== undefined) {
config.stopOnEntry = config.stopAtEntry;
delete config.stopAtEntry;
}
// Translate serverLaunchTimeout (msec) to timeout (seconds).
if (config.serverLaunchTimeout !== undefined) {
config.timeout = Math.floor(config.serverLaunchTimeout / 1000);
delete config.serverLaunchTimeout;
}
// Translate externalConsole to runInTerminal (!inverse).
if (config.externalConsole !== undefined) {
config.runInTerminal = !config.externalConsole;
delete config.externalConsole;
}
// TODO: translate sourceFileMap to sourcePath and sourceMap.
// Translate processId to pid.
if (config.processId !== undefined) {
config.pid = Number(config.processId);
delete config.program;
}
return config;
}
/** Connection to the worker thread handling lldb-dap related operations. */
let remote: RemoteConnection | undefined;
/** Cached path to the lldb-dap executable once found. */
let lldbDapExecutable: string | undefined;
/**
* Finds the path to the lldb-dap executable.
*
* This function uses a worker thread if available, otherwise calls the implementation directly.
* The result is cached for subsequent calls.
*
* @returns A promise that resolves to the path of the lldb-dap executable, or undefined if not found.
*/
export async function findLldbDap() {
return lldbDapExecutable ??= remote ? await remote.request('findLldbDapImpl') : findLldbDapImpl();
}
// This code must only run in the main thread.
if (isMainThread && !remote) {
try {
// Find the entry point for the worker thread.
const file = resolve(__dirname.substring(0, __dirname.lastIndexOf('dist')), "dist", "src", "Debugger", 'lldb-dap-worker.js');
// Create the worker and connection.
remote = startRemoting(startWorker(file), {
// These are the functions that the main thread exposes to the worker thread.
log,
note,
localize,
appendLine,
appendLineAtLevel
});
if (!isWindows) {
// If we are not on Windows, we'll start it searching for the lldb-dap executable as early as possible.
// Mainly, because the LLDB-DAP debugger isn't a common standalone debugger for Windows.
void findLldbDap();
}
} catch (e) {
// If we fail to start the worker, we can still use the implementation directly.
appendLineAtLevel(6, "Failed to start the worker thread for LLDB-DAP remote calls. Falling back to direct calls.");
remote = undefined;
}
}
+71 -40
View File
@@ -5,25 +5,44 @@
import * as child_process from 'child_process';
import * as os from 'os';
import { normalize } from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { findPowerShell } from '../common';
import { isMacOS } from '../constants';
import { AttachItem } from './attachQuickPick';
import { AttachItemsProvider } from './attachToProcess';
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
export class Process {
constructor(public name: string, public pid?: string, public commandLine?: string) { }
export class Process implements AttachItem {
get label() {
return this.name;
}
public toAttachItem(): AttachItem {
return {
label: this.name,
description: this.pid,
detail: this.commandLine,
id: this.pid
};
get description() {
// If the fullPath is known and different than the process name, put that in the description after the pid
// to let the user can find the actual process they are looking for.
return this.fullPath && this.fullPath !== this.name ? `${this.pid} [${this.fullPath}]` : this.pid;
}
get detail() {
return this.commandLine;
}
get id() {
return this.pid;
}
constructor(public name: string, public pid?: string, public commandLine?: string, public fullPath?: string) {
if (this.fullPath) {
// If we have a full path, clean it up.
if (this.fullPath?.startsWith('"') && this.fullPath.endsWith('"')) {
this.fullPath = this.fullPath.slice(1, -1);
}
this.fullPath = normalize(this.fullPath);
}
}
}
@@ -62,8 +81,7 @@ abstract class NativeAttachItemsProvider implements AttachItemsProvider {
}
return aLower < bLower ? -1 : 1;
});
const attachItems: AttachItem[] = processEntries.map(p => p.toAttachItem());
return attachItems;
return processEntries;
}
}
@@ -93,33 +111,26 @@ export class PsAttachItemsProvider extends NativeAttachItemsProvider {
// QuickPick UI in VSCode.
protected async getInternalProcessEntries(token?: vscode.CancellationToken): Promise<Process[]> {
let processCmd: string = '';
switch (os.platform()) {
case 'darwin':
processCmd = PsProcessParser.psDarwinCommand;
break;
return PsProcessParser.ParseProcessFromPs(await spawnChildProcess(PsProcessParser.psDarwinCommand, token));
case 'linux':
processCmd = PsProcessParser.psLinuxCommand;
break;
return PsProcessParser.ParseProcessFromPs(await spawnChildProcess(PsProcessParser.psLinuxCommand, token));
default:
throw new Error(localize("os.not.supported", 'Operating system "{0}" not supported.', os.platform()));
}
const processes: string = await spawnChildProcess(processCmd, token);
return PsProcessParser.ParseProcessFromPs(processes);
}
}
export class PsProcessParser {
private static get secondColumnCharacters(): number { return 50; }
private static get commColumnTitle(): string { return Array(PsProcessParser.secondColumnCharacters).join("a"); }
// the BSD version of ps uses '-c' to have 'comm' only output the executable name and not
// the full path. The Linux version of ps has 'comm' to only display the name of the executable
// Use a large fixed width - the default on macOS is quite small.
static fixedWidth = ''.padEnd(512, 'a');
// Note that comm on Linux systems is truncated to 16 characters:
// https://bugzilla.redhat.com/show_bug.cgi?id=429565
// Since 'args' contains the full path to the executable, even if truncated, searching will work as desired.
public static get psLinuxCommand(): string { return `ps axww -o pid=,comm=${PsProcessParser.commColumnTitle},args=`; }
public static get psDarwinCommand(): string { return `ps axww -o pid=,comm=${PsProcessParser.commColumnTitle},args= -c`; }
public static get psToyboxCommand(): string { return `ps -A -o pid=,comm=${PsProcessParser.commColumnTitle},args=`; }
public static get psLinuxCommand(): string { return `ps axww -o pid=,exe=${this.fixedWidth},args=${this.fixedWidth}`; }
public static get psDarwinCommand(): string { return `ps axww -o pid=,comm=${this.fixedWidth},args=${this.fixedWidth}`; }
public static get psToyboxCommand(): string { return `ps -A -o pid=,comm=${this.fixedWidth},args=${this.fixedWidth}`; }
// Only public for tests.
public static ParseProcessFromPs(processes: string): Process[] {
@@ -147,21 +158,40 @@ export class PsProcessParser {
}
private static parseLineFromPs(line: string): Process | undefined {
// Explanation of the regex:
// - any leading whitespace
// - PID
// - whitespace
// - executable name --> this is PsAttachItemsProvider.secondColumnCharacters - 1 because ps reserves one character
// for the whitespace separator
// - whitespace
// - args (might be empty)
const psEntry: RegExp = new RegExp(`^\\s*([0-9]+)\\s+(.{${PsProcessParser.secondColumnCharacters - 1}})\\s+(.*)$`);
const psEntry = isMacOS ?
// On macOS, we're using fixed-width columns, so we have to use a fixed-width regex.
// <start>whitespace(NUMBERS)whitespace(FIXED-WIDTH)whitespace(EVERYTHING-ELSE)<end>
new RegExp(`^\\s*([0-9]+)\\s+(.{${PsProcessParser.fixedWidth.length - 1}})\\s+(.*)$`) :
// On Linux, column widths cannot be guaranteed - but we do get escaped spaces in the command line.
// <start>whitespace(NUMBERS)whitespace(NOTWHITESPACE)whitespace(EVERYTHING-ELSE)<end>
/^\s*(\d+)\s+((?:\\\s|\S)+)\s+([\s\S]*)$/;
const matches: RegExpExecArray | null = psEntry.exec(line);
if (matches && matches.length === 4) {
const pid: string = matches[1].trim();
const executable: string = matches[2].trim();
const cmdline: string = matches[3].trim();
return new Process(executable, pid, cmdline);
let fullPath: string = matches[2].trim();
const rawCommandLine = matches[3].trim();
// Trim the full path off the command line so that we optimize for seeing '<cmd> <args>'.
const cmdline: string = rawCommandLine.replace(/^\s*[\/\\]*(?:(?:[^\\\/\s]|\\.)+[\/\\])*([^\\\/\s]+)(?=\s|$)/, "$1");
// If the fullPath == '-', let's grab the arg0 from the raw command line.
if (fullPath === '-') {
const args = /^((?:\\\s|\S)+)\s*([\s\S]*)$/.exec(rawCommandLine);
if (args) {
fullPath = args[1];
}
}
const executable: string = fullPath.replace(/^.*\//, '');
// Skip processes that are '<defunct>'.
if (executable === '<defunct>' || cmdline.includes('<defunct>')) {
return undefined;
}
return new Process(executable, pid, cmdline, fullPath);
}
}
}
@@ -337,7 +367,7 @@ export class CimAttachItemsProvider extends NativeAttachItemsProvider {
protected async getInternalProcessEntries(token?: vscode.CancellationToken): Promise<Process[]> {
const pwshCommand: string = `${this.pwsh} -NoProfile -Command`;
const cimCommand: string = 'Get-CimInstance Win32_Process | Select-Object Name,ProcessId,CommandLine | ConvertTo-JSON -Compress';
const cimCommand: string = 'Get-CimInstance Win32_Process | Select-Object Name,ProcessId,CommandLine,Path | ConvertTo-JSON -Compress';
const processes: string = await spawnChildProcess(`${pwshCommand} "${cimCommand}"`, token);
return CimProcessParser.ParseProcessFromCim(processes);
}
@@ -347,6 +377,7 @@ type CimProcessInfo = {
Name: string;
ProcessId: number;
CommandLine: string | null;
Path: string | null;
};
export class CimProcessParser {
@@ -364,7 +395,7 @@ export class CimProcessParser {
if (cmdline?.startsWith(this.ntObjectManagerPathPrefix)) {
cmdline = cmdline.slice(this.ntObjectManagerPathPrefix.length);
}
return new Process(info.Name, `${info.ProcessId}`, cmdline);
return new Process(info.Name, `${info.ProcessId}`, cmdline, info.Path || undefined);
});
}
}
+3 -2
View File
@@ -17,6 +17,7 @@ import { TargetPopulation } from 'vscode-tas-client';
import * as which from 'which';
import { logAndReturn } from '../Utility/Async/returns';
import * as util from '../common';
import { ProcessReturnType, spawnChildProcess } from '../common-remote-safe';
import { modelSelector } from '../constants';
import { instrument } from '../instrumentation';
import { getCrashCallStacksChannel } from '../logger';
@@ -1244,9 +1245,9 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr
let funcStr: string = line.substring(startPos2, offsetPos);
let origFuncStr: string = "";
if (filtPath && filtPath.length !== 0) {
let ret: util.ProcessReturnType | undefined = await util.spawnChildProcess(filtPath, ["--no-strip-underscore", funcStr], undefined, true).catch(logAndReturn.undefined);
let ret: ProcessReturnType | undefined = await spawnChildProcess(filtPath, ["--no-strip-underscore", funcStr], undefined, true).catch(logAndReturn.undefined);
if (ret?.output === funcStr) {
ret = await util.spawnChildProcess(filtPath, [funcStr], undefined, true).catch(logAndReturn.undefined);
ret = await spawnChildProcess(filtPath, [funcStr], undefined, true).catch(logAndReturn.undefined);
}
if (ret !== undefined && ret.succeeded && !ret.output.startsWith("Could not open input file")) {
origFuncStr = funcStr;
+2 -1
View File
@@ -6,7 +6,8 @@
import { constants } from 'fs';
import { TreeItem } from "vscode";
import * as nls from 'vscode-nls';
import { extensionContext, ISshConfigHostInfo, pathAccessible } from "../../common";
import { extensionContext, ISshConfigHostInfo } from "../../common";
import { pathAccessible } from '../../common-remote-safe';
import { LabelLeafNode } from "./common";
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
+2 -1
View File
@@ -5,7 +5,8 @@
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { getFullHostAddress, getFullHostAddressNoPort, ISshHostInfo, ISshLocalForwardInfo, ProcessReturnType } from '../common';
import { getFullHostAddress, getFullHostAddressNoPort, ISshHostInfo, ISshLocalForwardInfo } from '../common';
import { ProcessReturnType } from '../common-remote-safe';
import { defaultSystemInteractor } from './commandInteractors';
import { runSshTerminalCommandWithLogin } from './sshCommandRunner';
+2 -1
View File
@@ -9,7 +9,8 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { CppSettings } from '../LanguageServer/settings';
import { ManualPromise } from '../Utility/Async/manualPromise';
import { ISshHostInfo, ProcessReturnType, getNumericLoggingLevel, splitLines, stripEscapeSequences } from '../common';
import { ISshHostInfo, getNumericLoggingLevel, splitLines, stripEscapeSequences } from '../common';
import { ProcessReturnType } from '../common-remote-safe';
import { isWindows } from '../constants';
import { getSshChannel } from '../logger';
import {
+2 -2
View File
@@ -43,14 +43,14 @@ export class ManualPromise<T = void> implements Promise<T> {
* A method to manually resolve the Promise.
*/
public resolve: (value?: T | PromiseLike<T> | undefined) => void = (v) => {
void v; /* */
void v;
};
/**
* A method to manually reject the Promise
*/
public reject: (e: any) => void = (e) => {
void e; /* */
void e;
};
private state: 'pending' | 'resolved' | 'rejected' = 'pending';
+541
View File
@@ -0,0 +1,541 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { fail, ok } from 'node:assert';
import { isPromise } from 'node:util/types';
import { MessagePort, SHARE_ENV, Worker, isMainThread } from 'node:worker_threads';
import { ManualPromise } from '../Async/manualPromise';
import { finalize } from '../System/finalize';
import { is } from '../System/guards';
/*
* SNARE: Simple Nodejs Asynchronous Remoting Engine
*
* SNARE is an extremely lightweight remoting engine that
* allows you to call functions in a nodejs worker thread
*
* It supports:
* - notifications (no response)
* - requests (async calls with a response)
* - error handling
* - simple byref object management
*
* As long as the values you pass and return are supported by the Structured Clone Algorithm,
* (see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)
* you can pass them by value to the remote thread.
*
* If you need to pass an object by reference, you can manually craft a remote object extending
* the MarshalByReference.
*/
// Enable typescript disposable types/interfaces.
/// <reference lib="esnext.disposable" />
// Polyfill `Symbol.dispose`
(Symbol as any).dispose ??= Symbol("Symbol.dispose");
(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose");
const results = new Map<number, ManualPromise<any>>();
let next = 0;
/**
* Internal interface representing a message payload exchanged between threads.
* Contains operation names, sequence numbers, and associated data.
*/
interface Payload {
/** The operation to perform (function name or built-in command). */
operation: string;
/** Sequence number for matching requests with responses (0 for notifications). */
sequence: number;
/** Function parameters when sending a request. */
parameters?: any[];
/** Return value when sending a response. */
result?: any;
/** Error information when an operation fails. */
error?: any;
}
/**
* Represents a connection to a remote thread that can be used to invoke operations.
* Provides methods for making requests, sending notifications, and managing remote objects.
*/
export interface RemoteConnection {
/** The underlying Worker or MessagePort used for communication. */
connection: Worker | MessagePort;
/** Terminates the connection and cleans up resources. */
terminate(): void;
/**
* Makes a request to the remote thread and awaits a response.
*
* @param operation The operation name to invoke.
* @param parameters Parameters to pass to the remote operation.
* @returns A promise that resolves with the operation result.
*/
request(operation: string, ...parameters: any[]): Promise<any>;
/**
* Sends a notification to the remote thread (fire and forget).
*
* @param operation The operation name to invoke.
* @param parameters Parameters to pass to the remote operation.
*/
notify(operation: string, ...parameters: any[]): void;
/**
* Creates a proxy for a remote object.
*
* @param ctor Constructor for the proxy type.
* @param instance Promise of a remote object instance ID.
* @returns Promise of a proxy to the remote object.
*/
marshall<T extends MarshalByReference>(ctor: new (remote: RemoteConnection, instance: number) => T, instance: Promise<number>): Promise<T | undefined>;
/**
* Creates a proxy for a remote object.
*
* @param ctor Constructor for the proxy type.
* @param instance A remote object instance ID.
* @returns A proxy to the remote object.
*/
marshall<T extends MarshalByReference>(ctor: new (remote: RemoteConnection, instance: number) => T, instance: number): T | undefined;
}
/**
* Type representing a set of functions that can be called remotely.
* The endpoint serves as the API that can be invoked from the other thread.
*/
export type Endpoint = Record<string, (...args: any[]) => any>;
/** SNARE: Simple Nodejs Asynchronous Remoting Engine */
export function startRemoting(connection: Worker | MessagePort, endpoint: Endpoint): RemoteConnection { // Main thread needs to wait for the connection to be ready before doing anything.
// The worker threads don't have an 'online' event (the port is already connected).
let ready = isMainThread ? new ManualPromise<undefined>() : undefined;
connection.on('online', () => ready?.resolve());
function postResult(sequence: number, retVal: any) {
if (is.promise(retVal)) {
retVal.then(
(result) => connection.postMessage({ operation: "#result", sequence: sequence, result: sanitize(result) }),
(error) => connection.postMessage({ operation: "#error", sequence: sequence, error: sanitize(error) })); // Call failed, threw an error.
return;
}
connection.postMessage({ operation: "#result", sequence: sequence, result: sanitize(retVal) });
}
// Handle messages from the remote thread.
connection.on('message', ({ operation, sequence, parameters, result, error }: Payload) => {
// Unwrap incoming parameters that are byref or function references.
parameters = parameters?.map(unwrapValue) || []; // Switch based on the operation type.
switch (operation) {
// If the message is an error, reject the promise.
case '#error':
results.get(sequence)?.reject(error);
return results.delete(sequence);
// If the message is a result, resolve the promise.
case '#result':
results.get(sequence)?.resolve(unwrapValue(result));
return results.delete(sequence);
// Unref is a built-in function to release a byref object.
case '#unref':
unref(parameters[0]);
return;
// Callback is a built-in function to call a callback function.
case '#callback':
try {
postResult(sequence, getByRef(parameters[0])(...parameters.slice(1)));
} catch (err) {
connection.postMessage({ operation: "#error", sequence, error: sanitize(err) });
}
return;
}
// Otherwise, we're going to call a remote function.
// Get the sequence number for the result (0 indicates that it's a notification).
try {
// Call the endpoint.
if (!is.function(endpoint[operation])) {
throw new Error(`Attempting to call unknown remote method on endpoint: ${operation}`);
}
const retVal = endpoint[operation](...parameters);
if (sequence) {
// Is it a request? If so, post the result.
postResult(sequence, retVal);
}
} catch (err) {
if (sequence) {
connection.postMessage({ operation: "#error", sequence, error: sanitize(err) }); // Call failed, threw an error.
}
}
});
const remote = {
connection,
request: async (operation: string, ...parameters: any[]) => {
let result: ManualPromise<any> | undefined;
let payload: Payload | undefined;
try {
if (ready) {
await ready;
}
result = new ManualPromise<any>();
payload = { operation, parameters: sanitizeParameters(parameters), sequence: ++next };
results.set(payload.sequence, result);
connection.postMessage(payload);
} catch (err) {
// If we can't post the message, then we need to reject the promise.
if (payload) {
results.delete(payload.sequence);
}
if (result) {
result.reject(err);
}
}
return result;
},
notify: (operation: string, ...parameters: any[]) => {
try {
if (ready) {
void ready.then(() => {
ready = undefined;
connection.postMessage({ operation, parameters: sanitizeParameters(parameters), sequence: 0 });
});
} else {
connection.postMessage({ operation, parameters: sanitizeParameters(parameters), sequence: 0 });
}
} catch (err: any) {
// Ignore the errors on notifications, as they are not expected to return a result.
}
},
marshall: <T extends MarshalByReference>(ctor: new (remote: RemoteConnection, instance: number) => T, instance: number | Promise<number>) => instance ? is.promise(instance) ? instance.then(i => new ctor(remote, i)) : new ctor(remote, instance) : undefined,
terminate: () => isMainThread ? finalize(connection) : undefined
};
function unwrapValue(value: any) {
return isReference(value) ?
value.kind === 'function' ? (...args: any[]) => remote.request('#callback', value.identity, ...args) : // If it is a referenced function, we'll create a wrapper function to call it.
getByRef(value.identity) : // If it is a referenced object, we'll get the object.
value;
}
connection.on('close', () => { // Disable the remote connection interface so that it can't be used anymore.
const r = remote as any;
r.request = r.marshal = async () => { };
r.notify = r.terminate = () => { };
r.connection = undefined;
// The connection is closed, so reject all pending requests.
for (const result of results.values()) {
try {
result.reject('Connection closed');
} catch {
// Ignore errors when rejecting.
}
}
// Clear the results map.
results.clear();
// Clear out any byref objects.
identityIndex.length = 0;
instanceIndex.clear();
});
return remote;
}
/**
* Ensures that the current code is running on the main thread.
*
* @throws If not running on the main thread.
*/
export function ensureIsMainThread() {
ok(isMainThread, "Remoting: Failed to start host thread responder - not on main thread");
}
/**
* Starts a new worker thread.
*
* @param workerPath Path to the worker script file.
* @returns A new Worker instance with stderr, stdout, and environment variables shared.
*/
export function startWorker(workerPath: string) {
return new Worker(workerPath, { stderr: true, stdout: true, env: SHARE_ENV });
}
/**
* Returns a value that is safe to go over the connection.
*
* This includes:
* - functions (which are marshalled as references)
* - MarshalByReference objects (which are marshalled as references)
*
* Otherwise, we do a deep filtered clone of the object.
*
* @param value The value to sanitize
* @returns A sanitized version of the value that can be safely sent over the connection
*/
function sanitize(value: any) {
return is.function(value) || value instanceof MarshalByReference ? ref(value) : filteredClone(value);
}
/**
* Sanitizes an array of parameters for remoting.
*
* @param parameters Array of parameter values to sanitize.
* @returns Array of sanitized parameters.
*/
function sanitizeParameters(parameters: any[]) {
return parameters.map(sanitize);
}
/**
* Type guard to check if a value is a reference object.
* References are objects with identity and kind properties.
*
* @param p The value to check.
* @returns True if the value is a reference object.
*/
function isReference(p: any): p is Reference {
return is.object(p) && 'identity' in p && 'kind' in p;
}
/**
* Interface representing a reference to an object or function in another thread.
* References are used to represent values that cannot be cloned.
*/
interface Reference {
/** Unique identifier for the referenced object. */
identity: number;
/** The kind of reference - either an object or function. */
kind: 'object' | 'function';
}
/** Array of objects indexed by their identity number. */
const identityIndex = new Array<any>();
/** Maps objects to their [identity, reference count] pairs. */
const instanceIndex = new Map<any, [number, number]>();
/**
* Gets an object by its reference identity.
*
* @param identity The unique identifier of the referenced object.
* @returns The referenced object.
* @throws If the identity doesn't correspond to a valid referenced object.
*/
export function getByRef<T = any>(identity: number): T {
return identityIndex[identity] ?? fail(`Invalid ${identity} for ByRef object`);
}
/**
* Creates a reference to an object or function that can be sent to another thread.
*
* @param instance A Promise resolving to the object to reference.
* @returns A Promise resolving to a reference object, or undefined if the value can't be referenced.
*/
export function ref(instance: Promise<any>): Promise<Reference | undefined>;
/**
* Creates a reference to an object or function that can be sent to another thread.
*
* @param instance The object to reference.
* @returns A reference object, or undefined if the value can't be referenced.
*/
export function ref(instance: any): Reference | undefined;
/**
* Implementation of the ref function handling both synchronous and asynchronous cases.
*
* @param instance The object or Promise to reference.
* @returns A reference object, a Promise to a reference object, or undefined.
*/
export function ref(instance: any | Promise<any>): Reference | undefined | Promise<Reference | undefined> {
if (is.promise(instance)) {
return instance.then(ref);
}
const kind = typeof instance;
switch (kind) {
case 'object':
case 'function':
// Lookup the instance in the index.
const [identity, refcount] = instanceIndex.get(instance) ?? [++next, 0];
// If refcount is zero, then we need to add it to the index.
if (!refcount) {
identityIndex[identity] = instance;
}
// Increment the refcount.
instanceIndex.set(instance, [identity, refcount + 1]);
// Return the identity.
return { identity, kind };
}
// Otherwise, we can't ref it.
return undefined;
}
/**
* Decrements the reference count for a referenced object.
* If the reference count reaches zero, the object is removed from the index and finalized.
*
* @param identity The identity of the referenced object to unreference.
*/
export function unref(identity: number) {
// Lookup the instance.
const instance = getByRef(identity);
if (instance) {
// Decrement the refcount.
const [identity, refcount] = instanceIndex.get(instance) ?? [0, 0];
if (refcount > 1) {
// Reduce the refcount by one.
return instanceIndex.set(instance, [identity, refcount - 1]);
}
// It's the last reference, so remove it from the index.
identityIndex[identity] = undefined;
instanceIndex.delete(instance);
finalize(instance);
}
}
/**
* A base class for objects that are passed by reference to a remote thread.
*
* All MarshalByReference wrappers are references to an object that lives in the remote thread.
* It is important to call .dispose() when you are done with it, as this enables the remote
* thread to release the object and free up resources.
*/
export class MarshalByReference implements Disposable {
constructor(protected remote: RemoteConnection, protected instance: number) {
}
/**
* This disposes the ByRef object and notifies the remote thread to reduce the refcount,
* which would dispose the remote object if it was the last reference.
*/
[Symbol.dispose]() {
void this.remote.notify('#unref', this.instance);
this.instance = 0;
}
}
/**
* Returns a filtered structured clone of an object that can be safely transmitted between threads.
*
* This recursively drops items that are:
* - undefined
* - functions
* - symbols
* - promises
* - asyncIterators
*
* It should correctly handle:
* - circular references by keeping a map of references that have already been cloned
* - arrays, sets, maps, and iterables (iterables are converted to arrays)
* - dates, regex, errors, and buffers by returning them directly
* - vscode.Uri by converting it to a string
* - all other objects by recursively cloning them
*
* @param data The data to clone.
* @param references Map of already cloned objects (for handling circular references).
* @param options Additional options for controlling the cloning process.
* @returns A filtered clone of the input data suitable for transmission.
*/
export function filteredClone(data: any, references = new Map<any, any>(), options?: { breakCircular?: boolean }): any {
// Fast checks for null and undefined.
if (data === undefined || data === null) {
return undefined;
}
if (references.has(data)) {
return references.get(data);
}
switch (typeof data) {
case 'symbol':
case 'function':
return undefined;
case 'object':
if (options?.breakCircular) {
references.set(data, undefined);
}
if (isPromise(data) || typeof data.then === 'function' || typeof data[Symbol.asyncIterator] === 'function') {
return undefined;
}
if (data instanceof Set) {
const result = new Set();
references.set(data, result);
for (const item of data) {
result.add(filteredClone(item, references, options));
}
return result;
}
if (data instanceof Map) {
const result = new Map();
references.set(data, result);
for (const [key, value] of data) {
const k = filteredClone(key, references, options);
if (k !== undefined) {
result.set(k, filteredClone(value, references, options));
}
}
return result;
}
if (data[Symbol.iterator]) {
const result: any[] = [];
references.set(data, result);
for (const item of data) {
result.push(filteredClone(item, references, options));
} return result;
}
// These are ok to return the value directly.
if (data instanceof Date || data instanceof RegExp || data instanceof Error || data instanceof Buffer) {
return data;
}
// Special case for vscode.Uri - we'll just return the string.
if ('path' in data && 'scheme' in data && 'authority' in data && 'fragment' in data) {
return data.toString();
}
// Everything else is an object that we'll recursively clone.
const result: any = {};
if (!options?.breakCircular) {
references.set(data, result);
}
for (const key in data) {
if (key.startsWith('_')) {
continue;
}
const v = filteredClone(data[key], references, options);
if (v !== undefined) {
// Drop undefined values entirely.
result[key] = v;
}
}
return result;
}
return data;
}
+463
View File
@@ -0,0 +1,463 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
/** @file The functions here are safe to call from a worker thread or the main thread. */
import { ChildProcess, spawn } from 'node:child_process';
import { access, constants, readdir, stat } from 'node:fs/promises';
import { basename, delimiter, dirname, isAbsolute, normalize, resolve } from 'node:path';
import { isMainThread } from 'node:worker_threads';
import { isWindows } from './constants';
import { ManualPromise } from './Utility/Async/manualPromise';
import { RemoteConnection } from './Utility/Remoting/snare';
import { is } from './Utility/System/guards';
/**
* @file These are vscode-extension functions that have been wrapped
* so that they can be safely called from the main thread or worker threads.
*
* If this is imported in a module running in a worker thread, the worker
* thread must call `initialize(remote)` with a valid `RemoteConnection`
* instance before using these functions, otherwise they will be a no-op.
*/
let remoteConnection: RemoteConnection | undefined;
// In the main thread, grab the logger and localize functions directly.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const logFn = isMainThread ? require('./logger').log : () => { };
// eslint-disable-next-line @typescript-eslint/no-var-requires
const noteFn = isMainThread ? require('./logger').note : () => { };
// eslint-disable-next-line @typescript-eslint/no-var-requires
const localizeFn = isMainThread ? require('./localization').localize : (info: { key: string; comment: string[] } | string, message: string, ...args: (string | number | boolean | undefined | null)[]) => message.replace(/\{(\d+)\}/g, (_, index) => String(args[Number(index)] ?? 'undefined'));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const getOutputChannelLoggerFn = isMainThread ? require('./logger').getOutputChannelLogger : () => undefined;
/**
* Used when this is imported in a module running in a worker thread.
*
* This ensures that the remote connection is set up before any of the
* functions are called. If this is not called, the functions will be no-ops.
*
* @param remote The remote connection instance to use.
*/
export function initialize(remote: RemoteConnection | undefined): void {
if (!isMainThread) {
remoteConnection = remote;
}
}
/** Appends the message to the log file.
*
* @param message The message to log. If this is a Promise, it will be resolved and logged.
*/
export function log(message: string | Promise<string>) {
if (is.promise(message)) {
void message.then(log);
return;
}
return isMainThread ? logFn(message) : remoteConnection?.notify('log', message);
}
/** Sets a transient message in the vscode status bar.
*
* The mesage will be displayed for a short time and then cleared.
* @param message The message to display.
*/
export function note(message: string | Promise<string>) {
if (is.promise(message)) {
void message.then(note);
return;
}
return isMainThread ? noteFn(message) : remoteConnection?.notify('note', message);
}
/** Localizes a message.
*
* This function is used to localize a message in the vscode extension.
*
* @param info The localization information.
* @param message The message to localize.
* @param args The arguments for the message.
*/
export const localize: AsyncLocalizeFunc = (infoOrKey, message, ...args) => {
if (isMainThread) {
if (typeof infoOrKey === 'string') {
return localizeFn(infoOrKey, message, ...args);
} else {
return localizeFn(infoOrKey as LocalizeInfo, message, ...args);
}
}
return remoteConnection?.request('localize', infoOrKey, message, ...args) ??
message.replace(/\{(\d+)\}/g, (_, index) => String(args[Number(index)] ?? 'undefined'));
};
export function appendLineAtLevel(level: number, message: string | Promise<string>): void {
if (is.promise(message)) {
void message.then((msg) => appendLineAtLevel(level, msg));
return;
}
if (isMainThread) {
getOutputChannelLoggerFn()?.appendLineAtLevel(level, message);
} else {
remoteConnection?.notify('appendLineAtLevel', level, message);
}
}
export function appendLine(message: string | Promise<string>): void {
if (is.promise(message)) {
void message.then(appendLine);
return;
}
if (isMainThread) {
getOutputChannelLoggerFn()?.appendLine(message);
} else {
remoteConnection?.notify('appendLine', message);
}
}
/** When on Windows, ensures that the given executable name ends in an '.exe'.
* @param executableName The name of the executable to check.
* @returns The executable name with .exe appended if on Windows and the name does not already end in an '.exe'.
*/
export function executableName(executableName: string) {
return isWindows && !/\.exe$/i.test(executableName) ? `${executableName}.exe` : executableName;
}
/**
* Represents a type which can release resources, such
* as event listening or a timer.
*
* (This is similar to the `Disposable` interface in VS Code.)
*/
export interface Disposable {
/**
* Dispose this object.
*/
dispose(): any;
}
/**
* Represents a typed event.
*
* A function that represents an event to which you subscribe by calling it with
* a listener function as argument.
*
* (This is similar to the `Event` interface in VS Code.)
*
* @example
* item.onDidChange(function(event) { console.log("Event happened: " + event); });
*
* @param listener The listener function will be called when the event happens.
* @param thisArgs The `this`-argument which will be used when calling the event listener.
* @param disposables An array to which a {@link Disposable} will be added.
* @returns A disposable which unsubscribes the event listener.
*/
export type Event<T> = (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => Disposable;
/** The key for a localize call.
* This interface provides structure for localization information.
*/
export interface LocalizeInfo {
/** The localization key identifier. */
key: string;
/** Comments providing context for translators. */
comment: string[];
}
/** The type for the localize function for string localization, but can work asynchronously (can be called via remoting). */
export type AsyncLocalizeFunc = (
infoOrKey: LocalizeInfo | string,
message: string,
...args: (string | number | boolean | undefined | null)[]
) => string | Promise<string>;
/**
* A cancellation token is passed to an asynchronous or long running
* operation to request cancellation, like cancelling a request
* for completion items because the user continued to type.
*
* (This is similar to the `CancellationToken` interface in VS Code.)
*
* To get an instance of a `CancellationToken` use a
* {@link CancellationTokenSource}.
*/
export interface CancellationToken {
/**
* Is `true` when the token has been cancelled, `false` otherwise.
*/
isCancellationRequested: boolean;
/**
* An {@link Event} which fires upon cancellation.
*/
onCancellationRequested: Event<any>;
}
/** Searches the PATH for a given executable program, using an optional predicate to control if the candidate is accepted.
* @param filename The name of the executable to search for (string or a regular expression).
* @param predicate A function that takes a binary file path and returns a boolean indicating whether to include it in the results.
* @returns A promise that resolves to the full path of the executable if found, or undefined if not found.
*/
export async function findInPath(filename: string | RegExp, predicate?: (binary: string) => Promise<boolean>): Promise<string | undefined> {
return searchFolders(process.env["PATH"]?.split(delimiter) || [], filename, predicate, 0);
}
/** Checks if a path is accessible with the specified permissions.
*
* @param filePath The path to check for accessibility.
* @param permission fs file access constants: https://nodejs.org/api/fs.html#file-access-constants
* @returns A promise that resolves to true if the path is accessible with the specified permissions, false otherwise.
*/
export async function pathAccessible(filePath: string, permission: number = constants.F_OK): Promise<boolean> {
return filePath ? access(filePath, permission).then(() => true).catch(() => false) : false;
}
/** Checks if a file is executable by the current user.
* @param file The path to the file to check.
* @returns A promise that resolves to true if the file is executable, false otherwise.
*/
export function isExecutable(file: string): Promise<boolean> {
return pathAccessible(file, constants.X_OK);
}
/** Represents the result of spawning a child process.
* Contains information about the process exit code, standard output and error output.
*/
export interface ProcessReturnType {
succeeded: boolean;
exitCode?: number | NodeJS.Signals;
output: string;
outputError: string;
}
/** Escapes special characters in a string to make it safe for use in a regular expression.
* @param str The string to escape.
* @returns A new string with all special regex characters escaped with backslashes.
*/
export function escapeStringForRegex(str: string): string {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
}
/** Spawns a child process and returns its output.
* @param program The path to the program to execute.
* @param args The command line arguments to pass to the program.
* @param localizer The function to use for localization.
* @param continueOn Optional string pattern that, when found in stdout, will cause the process to be considered complete before it exits.
* @param skipLogging Optional flag to skip logging process output.
* @param cancellationToken Optional token that can be used to cancel the process.
* @returns A promise that resolves to an object containing process output and exit information.
*/
export async function spawnChildProcess(program: string, args: string[] = [], continueOn?: string, skipLogging?: boolean, cancellationToken?: CancellationToken): Promise<ProcessReturnType> {
// Do not use CppSettings to avoid circular require()
if (skipLogging === undefined || !skipLogging) {
appendLineAtLevel(5, `$ ${program} ${args.join(' ')}`);
}
const programOutput: ProcessOutput = await spawnChildProcessImpl(program, args, continueOn, skipLogging, cancellationToken);
const exitCode: number | NodeJS.Signals | undefined = programOutput.exitCode;
if (programOutput.exitCode) {
return { succeeded: false, exitCode, outputError: programOutput.stderr, output: programOutput.stderr || programOutput.stdout || await localize('process.exited', 'Process exited with code {0}', exitCode) };
} else {
let stdout: string;
if (programOutput.stdout.length) {
// Type system doesn't work very well here, so we need call toString
stdout = programOutput.stdout;
} else {
stdout = await localize('process.succeeded', 'Process executed successfully.');
}
return { succeeded: true, exitCode, outputError: programOutput.stderr, output: stdout };
}
}
/** Represents the output of a child process spawned by the extension.
* Contains the raw output streams and exit code information.
*/
interface ProcessOutput {
/** The exit code of the process, or the signal that terminated it. */
exitCode?: number | NodeJS.Signals;
/** The standard output of the process as a string. */
stdout: string;
/** The standard error output of the process as a string. */
stderr: string;
}
/** Implementation of the process spawning functionality.
* This function handles the actual creation and management of the child process.
*
* @param program The path to the program to execute.
* @param args The command line arguments to pass to the program.
* @param continueOn Optional string pattern that, when found in stdout, will cause the process to be considered complete before it exits.
* @param skipLogging Optional flag to skip logging process output.
* @param cancellationToken Optional token that can be used to cancel the process.
* @returns A promise that resolves to the process output, including stdout, stderr and exit code.
*/
async function spawnChildProcessImpl(program: string, args: string[], continueOn?: string, skipLogging?: boolean, cancellationToken?: CancellationToken): Promise<ProcessOutput> {
const result = new ManualPromise<ProcessOutput>();
let proc: ChildProcess;
if (await isExecutable(program)) {
proc = spawn(`.${isWindows ? '\\' : '/'}${basename(program)}`, args, { shell: true, cwd: dirname(program) });
} else {
proc = spawn(program, args, { shell: true });
}
const cancellationTokenListener: Disposable | undefined = cancellationToken?.onCancellationRequested(() => {
appendLine(localize('killing.process', 'Killing process {0}', program));
proc.kill();
});
/** Cleans up resources associated with the process.
* Removes all event listeners and disposes the cancellation token listener.
*/
const clean = () => {
proc.removeAllListeners();
if (cancellationTokenListener) {
cancellationTokenListener.dispose();
}
};
let stdout: string = '';
let stderr: string = '';
if (proc.stdout) {
proc.stdout.on('data', data => {
const str: string = data.toString();
if (skipLogging === undefined || !skipLogging) {
appendLineAtLevel(1, str);
}
stdout += str;
if (continueOn) {
const continueOnReg: string = escapeStringForRegex(continueOn);
if (stdout.search(continueOnReg)) {
result.resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
}
}
});
}
if (proc.stderr) {
proc.stderr.on('data', data => stderr += data.toString());
}
proc.on('close', (code, signal) => {
clean();
result.resolve({ exitCode: code || signal || undefined, stdout: stdout.trim(), stderr: stderr.trim() });
});
proc.on('error', error => {
clean();
result.reject(error);
});
return result;
}
/** Returns true if the path is a folder.
*
* @param path The path to check.
* @returns A promise that resolves to true if the path is a folder, false otherwise.
*/
export async function isFolder(path: string) {
try {
return (await stat(path)).isDirectory();
} catch {
// Ignore errors, if we can't access the path, it's not a folder.
}
return false;
}
export async function searchFolders(folders: string[], filename: string | RegExp, predicate?: (binary: string) => Promise<boolean>, maxDepth = 4, options: { result?: string; visited?: Set<string>; topDepth?: number } = {}): Promise<string | undefined> {
options.topDepth ??= maxDepth;
options.visited ??= new Set<string>();
const nameMatches = is.string(filename) ? (item: string) => item === filename : (item: string) => filename.test(item);
for (let folder of folders) {
// If the result was reached from anything going on asynchronously, we can stop searching.
if (options.result) {
return options.result;
}
// Ensure that the folder is normalized and absolute.
folder = normalize(folder);
if (!isAbsolute(folder)) {
continue;
}
// If we've been here before, skip this folder.
if (options.visited.has(folder) || !await isFolder(folder)) {
continue;
}
// Mark the folder as visited to avoid infinite loops.
options.visited.add(folder);
// We can do a quick check in the folder when the filename is a string.
if (is.string(filename)) {
const fullPath = resolve(folder, filename);
if (predicate ? await predicate(fullPath) : true) {
return options.result ??= fullPath;
}
// And, if maxDepth is 0, we don't need to bother searching subfolders at all.
if (maxDepth === 0) {
continue;
}
}
try {
const subfolders = new Array<string>();
// Parallelize the search for files in the folder.
await Promise.all((await readdir(folder).catch(() => [])).map(async (item) => {
// If we already found a match, stop searching.
if (options.result) {
return;
}
// If we are not going to search subfolders, we can skip this item if it doesn't match the filename.
if (maxDepth === 0 && !nameMatches(item)) {
return;
}
const fullPath: string = resolve(folder, item);
const stats = await stat(fullPath).catch(() => undefined);
if (!stats) {
return;
}
switch (true) {
// If anything else found anything, we can stop searching.
case !!options.result:
return;
// If the path is a symlink, skip it entirely.
case stats.isSymbolicLink():
// If it's a symlink, we can't follow it, so skip it.
log(`Skipping symlink: ${fullPath}`);
return;
// If it is a file, check for a match.
case stats.isFile():
if (nameMatches(item) && (predicate ? await predicate(fullPath) : true)) {
return options.result ??= fullPath;
}
break;
// If it's a folder, and we're not at the max depth yet, add it to the list of subfolders to search.
case maxDepth && stats.isDirectory():
subfolders.push(fullPath);
break;
}
}));
if (subfolders.length) {
await searchFolders(subfolders, filename, predicate, maxDepth - 1, options);
}
} catch {
// Skip folders that can't be accessed.
}
}
return options.result;
}
+1 -100
View File
@@ -15,7 +15,7 @@ import { DocumentFilter, Range } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { TargetPopulation } from 'vscode-tas-client';
import * as which from "which";
import { ManualPromise } from './Utility/Async/manualPromise';
import { isExecutable } from './common-remote-safe';
import { isWindows } from './constants';
import { getOutputChannelLogger, showOutputChannel } from './logger';
import { PlatformInformation } from './platform';
@@ -749,105 +749,6 @@ export function execChildProcess(process: string, workingDirectory?: string, cha
});
}
export interface ProcessReturnType {
succeeded: boolean;
exitCode?: number | NodeJS.Signals;
output: string;
outputError: string;
}
export async function spawnChildProcess(program: string, args: string[] = [], continueOn?: string, skipLogging?: boolean, cancellationToken?: vscode.CancellationToken): Promise<ProcessReturnType> {
// Do not use CppSettings to avoid circular require()
if (skipLogging === undefined || !skipLogging) {
getOutputChannelLogger().appendLineAtLevel(5, `$ ${program} ${args.join(' ')}`);
}
const programOutput: ProcessOutput = await spawnChildProcessImpl(program, args, continueOn, skipLogging, cancellationToken);
const exitCode: number | NodeJS.Signals | undefined = programOutput.exitCode;
if (programOutput.exitCode) {
return { succeeded: false, exitCode, outputError: programOutput.stderr, output: programOutput.stderr || programOutput.stdout || localize('process.exited', 'Process exited with code {0}', exitCode) };
} else {
let stdout: string;
if (programOutput.stdout.length) {
// Type system doesn't work very well here, so we need call toString
stdout = programOutput.stdout;
} else {
stdout = localize('process.succeeded', 'Process executed successfully.');
}
return { succeeded: true, exitCode, outputError: programOutput.stderr, output: stdout };
}
}
interface ProcessOutput {
exitCode?: number | NodeJS.Signals;
stdout: string;
stderr: string;
}
async function spawnChildProcessImpl(program: string, args: string[], continueOn?: string, skipLogging?: boolean, cancellationToken?: vscode.CancellationToken): Promise<ProcessOutput> {
const result = new ManualPromise<ProcessOutput>();
let proc: child_process.ChildProcess;
if (await isExecutable(program)) {
proc = child_process.spawn(`.${isWindows ? '\\' : '/'}${path.basename(program)}`, args, { shell: true, cwd: path.dirname(program) });
} else {
proc = child_process.spawn(program, args, { shell: true });
}
const cancellationTokenListener: vscode.Disposable | undefined = cancellationToken?.onCancellationRequested(() => {
getOutputChannelLogger().appendLine(localize('killing.process', 'Killing process {0}', program));
proc.kill();
});
const clean = () => {
proc.removeAllListeners();
if (cancellationTokenListener) {
cancellationTokenListener.dispose();
}
};
let stdout: string = '';
let stderr: string = '';
if (proc.stdout) {
proc.stdout.on('data', data => {
const str: string = data.toString();
if (skipLogging === undefined || !skipLogging) {
getOutputChannelLogger().appendAtLevel(1, str);
}
stdout += str;
if (continueOn) {
const continueOnReg: string = escapeStringForRegex(continueOn);
if (stdout.search(continueOnReg)) {
result.resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
}
}
});
}
if (proc.stderr) {
proc.stderr.on('data', data => stderr += data.toString());
}
proc.on('close', (code, signal) => {
clean();
result.resolve({ exitCode: code || signal || undefined, stdout: stdout.trim(), stderr: stderr.trim() });
});
proc.on('error', error => {
clean();
result.reject(error);
});
return result;
}
/**
* @param permission fs file access constants: https://nodejs.org/api/fs.html#file-access-constants
*/
export function pathAccessible(filePath: string, permission: number = fs.constants.F_OK): Promise<boolean> {
if (!filePath) { return Promise.resolve(false); }
return new Promise(resolve => fs.access(filePath, permission, err => resolve(!err)));
}
export function isExecutable(file: string): Promise<boolean> {
return pathAccessible(file, fs.constants.X_OK);
}
export async function allowExecution(file: string): Promise<void> {
if (process.platform !== 'win32') {
const exists: boolean = await checkFileExists(file);
+9
View File
@@ -0,0 +1,9 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
// Centralized https links that are used by the extension.
/** Link to documentation on how to troubleshoot LLDB-DAP. */
export const TroubleshootingLldbDap = "https://aka.ms/vscode-cpptools/TroubleshootingLldbDap";
+8
View File
@@ -0,0 +1,8 @@
/* --------------------------------------------------------------------------------------------
* 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 })();
export const localize: nls.LocalizeFunc = nls.loadMessageBundle();
+5
View File
@@ -193,3 +193,8 @@ export function disposeOutputChannels(): void {
warningChannel.dispose();
}
}
/** Sets a transient message in the vscode status bar. */
export function note(message: string) {
vscode.window.setStatusBarMessage(message, 5000);
}
@@ -8,10 +8,11 @@
import * as assert from 'assert';
import { suite } from 'mocha';
import * as vscode from 'vscode';
import { DebuggerType } from '../../../../src/Debugger/configurations';
suite(`Debug Integration Test: `, function(): void {
suite(`Debug Integration Test: `, function (): void {
suiteSetup(async function(): Promise<void> {
suiteSetup(async function (): Promise<void> {
const extension: vscode.Extension<any> = vscode.extensions.getExtension("ms-vscode.cpptools") || assert.fail("Extension not found");
if (!extension.isActive) {
await extension.activate();
@@ -27,7 +28,7 @@ suite(`Debug Integration Test: `, function(): void {
});
try {
assert.equal(vscode.debug.activeDebugSession?.type, "cppdbg");
assert.equal(vscode.debug.activeDebugSession?.type, DebuggerType.cppdbg);
} catch (e) {
assert.fail("Debugger failed to launch. Did the extension activate correctly?");
}
@@ -12,40 +12,40 @@ import { LinuxDistribution } from '../../../../src/linuxDistribution';
suite("LinuxDistro Tests", () => {
test("Parse valid os-release file", () => {
const dataUbuntu1404: string = 'NAME="Ubuntu"' + os.EOL +
'VERSION="14.04.4 LTS, Trusty Tahr"' + os.EOL +
'ID=ubuntu' + os.EOL +
'ID_LIKE=debian' + os.EOL +
'PRETTY_NAME="Ubuntu 14.04.4 LTS"' + os.EOL +
'VERSION_ID="14.04"' + os.EOL +
'HOME_URL="http://www.ubuntu.com/"' + os.EOL +
'SUPPORT_URL="http://help.ubuntu.com/"' + os.EOL +
'BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"';
'VERSION="14.04.4 LTS, Trusty Tahr"' + os.EOL +
'ID=ubuntu' + os.EOL +
'ID_LIKE=debian' + os.EOL +
'PRETTY_NAME="Ubuntu 14.04.4 LTS"' + os.EOL +
'VERSION_ID="14.04"' + os.EOL +
'HOME_URL="http://www.ubuntu.com/"' + os.EOL +
'SUPPORT_URL="http://help.ubuntu.com/"' + os.EOL +
'BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"';
const dataUbuntu1510: string = 'NAME="Ubuntu"' + os.EOL +
'VERSION="15.10 (Wily Werewolf)"' + os.EOL +
'ID=ubuntu' + os.EOL +
'ID_LIKE=debian' + os.EOL +
'PRETTY_NAME="Ubuntu 15.10"' + os.EOL +
'VERSION_ID="15.10"' + os.EOL +
'HOME_URL="http://www.ubuntu.com/"' + os.EOL +
'SUPPORT_URL="http://help.ubuntu.com/"' + os.EOL +
'BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"';
'VERSION="15.10 (Wily Werewolf)"' + os.EOL +
'ID=ubuntu' + os.EOL +
'ID_LIKE=debian' + os.EOL +
'PRETTY_NAME="Ubuntu 15.10"' + os.EOL +
'VERSION_ID="15.10"' + os.EOL +
'HOME_URL="http://www.ubuntu.com/"' + os.EOL +
'SUPPORT_URL="http://help.ubuntu.com/"' + os.EOL +
'BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"';
const dataCentos73: string = 'NAME="CentOS Linux"' + os.EOL +
'VERSION="7 (Core)"' + os.EOL +
'ID="centos"' + os.EOL +
'ID_LIKE="rhel fedora"' + os.EOL +
'VERSION_ID="7"' + os.EOL +
'PRETTY_NAME="CentOS Linux 7 (Core)"' + os.EOL +
'ANSI_COLOR="0;31"' + os.EOL +
'CPE_NAME="cpe:/o:centos:centos:7"' + os.EOL +
'HOME_URL="https://www.centos.org/"' + os.EOL +
'BUG_REPORT_URL="https://bugs.centos.org/"' + os.EOL +
os.EOL +
'CENTOS_MANTISBT_PROJECT="CentOS-7"' + os.EOL +
'CENTOS_MANTISBT_PROJECT_VERSION="7"' + os.EOL +
'REDHAT_SUPPORT_PRODUCT="centos"' + os.EOL +
'REDHAT_SUPPORT_PRODUCT_VERSION="7"';
'VERSION="7 (Core)"' + os.EOL +
'ID="centos"' + os.EOL +
'ID_LIKE="rhel fedora"' + os.EOL +
'VERSION_ID="7"' + os.EOL +
'PRETTY_NAME="CentOS Linux 7 (Core)"' + os.EOL +
'ANSI_COLOR="0;31"' + os.EOL +
'CPE_NAME="cpe:/o:centos:centos:7"' + os.EOL +
'HOME_URL="https://www.centos.org/"' + os.EOL +
'BUG_REPORT_URL="https://bugs.centos.org/"' + os.EOL +
os.EOL +
'CENTOS_MANTISBT_PROJECT="CentOS-7"' + os.EOL +
'CENTOS_MANTISBT_PROJECT_VERSION="7"' + os.EOL +
'REDHAT_SUPPORT_PRODUCT="centos"' + os.EOL +
'REDHAT_SUPPORT_PRODUCT_VERSION="7"';
const ubuntu1404: LinuxDistribution = LinuxDistribution.getDistroInformation(dataUbuntu1404);
const ubuntu1510: LinuxDistribution = LinuxDistribution.getDistroInformation(dataUbuntu1510);
@@ -74,18 +74,18 @@ suite("Pick Process Tests", () => {
test("Parse valid wmic output", () => {
// output from the command used in WmicAttachItemsProvider
const wmicOutput: string = 'CommandLine=' + os.EOL +
'Name=System Idle Process' + os.EOL +
'ProcessId=0' + os.EOL +
'' + os.EOL +
'' + os.EOL +
'CommandLine="C:\\Program Files (x86)\\Microsoft Office\\root\\Office16\\ONENOTE.EXE"' + os.EOL +
'Name=ONENOTE.EXE' + os.EOL +
'ProcessId=6540' + os.EOL +
'' + os.EOL +
'' + os.EOL +
`CommandLine=\\??\\C:\\windows\\system32\\conhost.exe 0x4` + os.EOL +
'Name=conhost.exe' + os.EOL +
'ProcessId=59148' + os.EOL;
'Name=System Idle Process' + os.EOL +
'ProcessId=0' + os.EOL +
'' + os.EOL +
'' + os.EOL +
'CommandLine="C:\\Program Files (x86)\\Microsoft Office\\root\\Office16\\ONENOTE.EXE"' + os.EOL +
'Name=ONENOTE.EXE' + os.EOL +
'ProcessId=6540' + os.EOL +
'' + os.EOL +
'' + os.EOL +
`CommandLine=\\??\\C:\\windows\\system32\\conhost.exe 0x4` + os.EOL +
'Name=conhost.exe' + os.EOL +
'ProcessId=59148' + os.EOL;
const parsedOutput: Process[] = WmicProcessParser.ParseProcessFromWmic(wmicOutput);
@@ -108,9 +108,9 @@ suite("Pick Process Tests", () => {
test("Parse valid ps output", () => {
// output from the command used in PsAttachItemsProvider
const psOutput: string = ' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + os.EOL +
'15470 ScopedBookmarkAgent ScopedBookmarkAgent' + os.EOL +
'15220 mdworker mdworker -s mdworker -c MDSImporterWorker -m com.apple.mdworker.shared' + os.EOL;
const psOutput: string = ' ' + PsProcessParser.fixedWidth + os.EOL +
'15470 ' + "ScopedBookmarkAgent".padEnd(512, ' ') + 'ScopedBookmarkAgent' + os.EOL +
'15220 ' + "mdworker".padEnd(512, ' ') + 'mdworker -s mdworker -c MDSImporterWorker -m com.apple.mdworker.shared' + os.EOL;
const parsedOutput: Process[] = PsProcessParser.ParseProcessFromPs(psOutput);
+408 -12
View File
@@ -47,11 +47,9 @@
"default": {}
},
"quoteArgs": {
"exceptions": {
"type": "boolean",
"description": "%c_cpp.debuggers.pipeTransport.quoteArgs.description%",
"default": true
}
"type": "boolean",
"description": "%c_cpp.debuggers.pipeTransport.quoteArgs.description%",
"default": true
}
}
},
@@ -604,7 +602,7 @@
"program": {
"type": "string",
"description": "%c_cpp.debuggers.program.description%",
"default": "${workspaceRoot}/a.out"
"default": "${workspaceFolder}/a.out"
},
"args": {
"type": "array",
@@ -839,7 +837,7 @@
"program": {
"type": "string",
"description": "%c_cpp.debuggers.program.description%",
"default": "${workspaceRoot}/a.out"
"default": "${workspaceFolder}/a.out"
},
"targetArchitecture": {
"type": "string",
@@ -951,17 +949,16 @@
}
}
},
"CppvsdbgLaunchOptions": {
"CpplldbLaunchOptions": {
"type": "object",
"required": [
"program",
"cwd"
"program"
],
"properties": {
"program": {
"type": "string",
"description": "%c_cpp.debuggers.program.description%",
"default": "${workspaceRoot}/program.exe"
"default": "${workspaceFolder}/a.out"
},
"args": {
"type": "array",
@@ -974,7 +971,401 @@
"cwd": {
"type": "string",
"description": "%c_cpp.debuggers.cwd.description%",
"default": "${workspaceRoot}"
"default": "."
},
"debuggerPath": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.debuggerPath.description%",
"default": "lldb-dap"
},
"debuggerArgs": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.debuggerArgs.description%",
"default": ""
},
"environment": {
"type": "array",
"description": "%c_cpp.debuggers.environment.description%",
"items": {
"$ref": "#/definitions/KeyValuePair"
},
"default": []
},
"envFile": {
"type": "string",
"description": "%c_cpp.debuggers.envFile.description%",
"default": "${workspaceFolder}/.env"
},
"stopAtEntry": {
"type": "boolean",
"markdownDescription": "%c_cpp.debuggers.stopAtEntry.markdownDescription%",
"default": false
},
"serverLaunchTimeout": {
"type": "integer",
"description": "%c_cpp.debuggers.serverLaunchTimeout.description%",
"default": "10000"
},
"externalConsole": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.externalConsole.description%",
"default": false
},
"sourceFileMap": {
"markdownDescription": "%c_cpp.debuggers.sourceFileMap.markdownDescription%",
"anyOf": [
{
"type": "object",
"default": {
"<source-path>": "<target-path>"
}
},
{
"$ref": "#/definitions/SourceFileMapEntry"
}
]
},
"deploySteps": {
"$ref": "#/definitions/DeploySteps"
},
"disableASLR": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.disableAslr.description%",
"default": true
},
"disableSTDIO": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.disableStdio.description%",
"default": false
},
"shellExpandArguments": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.expandShellArguments.description%",
"default": false
},
"detachOnError": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.detachOnError.description%",
"default": false
},
"debuggerRoot": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.debuggerRoot.description%"
},
"targetTriple": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.targetTriple.description%"
},
"platformName": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.platformName.description%"
},
"initCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.initCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"preRunCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.preRunCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"postRunCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.postRunCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"launchCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.launchCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"stopCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.stopCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"exitCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.exitCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"terminateCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.terminateCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"enableAutoVariableSummaries": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.enableAutoVariableSummaries.description%",
"default": false
},
"displayExtendedBacktrace": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.displayExtendedBacktrace.description%",
"default": false
},
"enableSyntheticChildDebugging": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.enableSyntheticChildDebugging.description%",
"default": false
},
"commandEscapePrefix": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.commandEscapePrefix.description%",
"default": "`"
},
"customFrameFormat": {
"type": "string",
"markdownDescription": "%c_cpp.debuggers.cpplldb.customFrameFormat.markdownDescription%",
"default": ""
},
"customThreadFormat": {
"type": "string",
"markdownDescription": "%c_cpp.debuggers.cpplldb.customThreadFormat.markdownDescription%",
"default": ""
}
}
},
"CpplldbAttachOptions": {
"type": "object",
"default": {},
"properties": {
"program": {
"type": "string",
"description": "%c_cpp.debuggers.program.description%",
"default": "${workspaceFolder}/a.out"
},
"debuggerPath": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldbdebuggerPath.description%",
"default": "lldb-dap"
},
"debuggerArgs": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.debuggerArgs.description%",
"default": ""
},
"processId": {
"markdownDescription": "%c_cpp.debuggers.processId.anyOf.markdownDescription%",
"anyOf": [
{
"type": "string",
"default": "${command:pickProcess}"
},
{
"type": "integer",
"default": 0
}
]
},
"sourceFileMap": {
"markdownDescription": "%c_cpp.debuggers.sourceFileMap.markdownDescription%",
"anyOf": [
{
"type": "object",
"default": {
"<source-path>": "<target-path>"
}
},
{
"$ref": "#/definitions/SourceFileMapEntry"
}
]
},
"serverLaunchTimeout": {
"type": "integer",
"description": "%c_cpp.debuggers.serverLaunchTimeout.description%",
"default": "10000"
},
"waitFor": {
"type": "boolean",
"markdownDescription": "%c_cpp.debuggers.cpplldb.waitFor.markdownDescription%",
"default": true
},
"deploySteps": {
"$ref": "#/definitions/DeploySteps"
},
"disableASLR": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.disableAslr.description%",
"default": true
},
"disableSTDIO": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.disableStdio.description%",
"default": false
},
"shellExpandArguments": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.expandShellArguments.description%",
"default": false
},
"detachOnError": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.detachOnError.description%",
"default": false
},
"debuggerRoot": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.debuggerRoot.description%"
},
"targetTriple": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.targetTriple.description%"
},
"platformName": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.platformName.description%"
},
"attachCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.platformName.description%",
"items": {
"type": "string"
},
"default": []
},
"initCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.initCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"preRunCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.preRunCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"postRunCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.postRunCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"stopCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.stopCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"exitCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.exitCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"terminateCommands": {
"type": "array",
"description": "%c_cpp.debuggers.cpplldb.terminateCommands.description%",
"items": {
"type": "string"
},
"default": []
},
"coreFile": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.coreFile.description%"
},
"gdb-remote-port": {
"type": [
"number",
"string"
],
"markdownDescription": "%c_cpp.debuggers.cpplldb.gdbRemotePort.markdownDescription%"
},
"gdb-remote-hostname": {
"type": "string",
"markdownDescription": "%c_cpp.debuggers.cpplldb.gdbRemoteHost.markdownDescription%"
},
"enableAutoVariableSummaries": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.enableAutoVariableSummaries.description%",
"default": false
},
"displayExtendedBacktrace": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.displayExtendedBacktrace.description%",
"default": false
},
"enableSyntheticChildDebugging": {
"type": "boolean",
"description": "%c_cpp.debuggers.cpplldb.enableSyntheticChildDebugging.description%",
"default": false
},
"commandEscapePrefix": {
"type": "string",
"description": "%c_cpp.debuggers.cpplldb.commandEscapePrefix.description%",
"default": "`"
},
"customFrameFormat": {
"type": "string",
"markdownDescription": "%c_cpp.debuggers.cpplldb.customFrameFormat.markdownDescription%",
"default": ""
},
"customThreadFormat": {
"type": "string",
"markdownDescription": "%c_cpp.debuggers.cpplldb.customThreadFormat.markdownDescription%",
"default": ""
}
}
},
"CppvsdbgLaunchOptions": {
"type": "object",
"required": [
"program",
"cwd"
],
"properties": {
"program": {
"type": "string",
"description": "%c_cpp.debuggers.program.description%",
"default": "${workspaceFolder}/program.exe"
},
"args": {
"type": "array",
"description": "%c_cpp.debuggers.args.description%",
"items": {
"type": "string"
},
"default": []
},
"cwd": {
"type": "string",
"description": "%c_cpp.debuggers.cwd.description%",
"default": "${workspaceFolder}"
},
"environment": {
"type": "array",
@@ -1102,6 +1493,11 @@
"processId"
],
"properties": {
"program": {
"type": "string",
"description": "%c_cpp.debuggers.program.description%",
"default": "${workspaceFolder}/program.exe"
},
"symbolSearchPath": {
"type": "string",
"description": "%c_cpp.debuggers.symbolSearchPath.description%",