Add script mode and ability to connect to externally launched CMake process. (#3277)

* storing initial thoughts

* rough version of trying to work on new options for debugger configurations

* made progress enabling script debugging from launch.json

* add some output messaging regarding the debugger and script

* add TODO's in case I don't get to it today

* log statements, ensure package.json allows the right args in right situation, set up env for script debugging

* push worst case copying the description and settings of the debugger options

* I think I've covered all launch config cases

* better package.json schema, though still not perfect, matches other debug types, and stub validation in code

* localize error messages

* add 'The'

* switch to double quotes

* add docs page for debugging

* slight modifications

* didn't handle case where scriptEnv was undefined

* add configurationSnippets

* add stub for debugconfigurationprovider

* add ability to 'run and debug' without launch.json on *.cmake files

* modify when we sanity check

* make adjustments based on feedback and add automatic configuration

* ensure that configure with debugger works with the right format

* update changelog
This commit is contained in:
Garrett Campbell 2023-09-20 13:20:31 -04:00 коммит произвёл GitHub
Родитель b4bd31490b
Коммит 661e9ed3e3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 469 добавлений и 35 удалений

Просмотреть файл

@ -8,6 +8,7 @@ Features:
Improvements:
- Updated debugging documentation to add the LLDB configuration needed for macOS. [PR #3332](https://github.com/microsoft/vscode-cmake-tools/pull/3332) [@slhck](https://github.com/slhck)
- In multi-root workspace, the Project Outline View now shows all configured projects. [PR #3270](https://github.com/microsoft/vscode-cmake-tools/pull/3270) [@vlavati](https://github.com/vlavati)
- Added script mode and ability to connect to externally launched CMake processes. [PR #3277](https://github.com/microsoft/vscode-cmake-tools/pull/3277)
## 1.15
Features:

Просмотреть файл

@ -41,6 +41,7 @@ CMake Tools is an extension designed to make it easy to work with CMake-based pr
* [Quick debugging](debug-launch.md#quick-debugging)
* [Debug using a launch.json file](debug-launch.md#debug-using-a-launchjson-file)
* [Run without debugging](debug-launch.md#run-without-debugging)
* [Debugging CMake](debug.md)
[Configure CMake Tools settings](cmake-settings.md)
* [CMake Tools settings](cmake-settings.md#cmake-settings)

72
docs/debug.md Normal file
Просмотреть файл

@ -0,0 +1,72 @@
# CMake Debugging
Starting with CMake 3.27, debugging CMake is supported in CMake Tools.
The following documentation will help you understand the various ways you can debug CMake scripts and cache generation.
## Debugging from CMake Tools UI entry points
The most common reason to debug CMake scripts and cache generation is to debug CMake cache generation. There are many ways that you can accomplish this:
* Commands
* CMake: Configure with CMake Debugger
* CMake: Delete Cache and Reconfigure with CMake Debugger
* Folder Explorer
* Right click on CMakeLists.txt -> Configure All Projects with CMake Debugger.
* Project Outline
* Right click on CMakeLists.txt -> Configure All Projects with CMake Debugger.
* Expand the "..." in the project outline. There is an entry to use the Debugger.
## Debugging from launch.json
CMake Tools provides a new debug type `cmake`.
The `cmake` debug type supports three different types of `cmakeDebugType`: `configure`, `external`, `script`. They each come with their own settings that can be used to modify and control the debug session.
### Example launch.json
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "cmake",
"request": "launch",
"name": "CMake script debugging",
"cmakeDebugType": "script",
"scriptPath": "${workspaceFolder}/<script>.cmake"
},
{
"type": "cmake",
"request": "launch",
"name": "Debug externally launched CMake process",
"cmakeDebugType": "external",
"pipeName": "<insert-pipe-name>"
}
]
}
```
Listed below are the settings that are available for each configuration based on `cmakeDebugType`:
* `configure`
* required: none
* optional
* `pipeName` - Name of the pipe (on Windows) or domain socket (on Unix) to use for debugger communication.
* `clean` - Clean prior to configuring.
* `configureAll` - Configure for all projects.
* `dapLog` - Where the debug adapter protocol (DAP) communication should be logged. If omitted, DAP communication is not logged.
* `external`
* required
* `pipeName` - Name of the pipe (on Windows) or domain socket (on Unix) to use for debugger communication.
* optional
* `script`
* required
* `scriptPath` - The path to the CMake script to debug.
* optional
* `scriptArgs` - Arguments for the CMake script to debug.
* `scriptEnv` - Environment for the CMake script to use.
* `pipeName` - Name of the pipe (on Windows) or domain socket (on Unix) to use for debugger communication.
* `dapLog` - Where the debug adapter protocol (DAP) communication should be logged. If omitted, DAP communication is not logged.
The `cmake` debug type only supports the `request` type: `launch`.

Просмотреть файл

@ -24,7 +24,8 @@
"vscode": "^1.63.0"
},
"categories": [
"Other"
"Other",
"Debuggers"
],
"galleryBanner": {
"color": "#13578c",
@ -49,6 +50,9 @@
"onCommand:cmake.executableTargets",
"onCommand:cmake.buildKit",
"onCommand:cmake.tasksBuildCommand",
"onDebugResolve:cmake",
"onDebugInitialConfigurations",
"onDebugDynamicConfigurations:cmake",
"workspaceContains:CMakeLists.txt",
"workspaceContains:*/CMakeLists.txt",
"workspaceContains:*/*/CMakeLists.txt",
@ -692,7 +696,7 @@
},
"options": {
"type": "object",
"description": "%cmake-tools.taskDefinitions.properties.options.description",
"description": "%cmake-tools.taskDefinitions.properties.options.description%",
"properties": {
"cwd": {
"type": "string",
@ -720,12 +724,52 @@
{
"type": "cmake",
"label": "%cmake-tools.debugger.label%",
"languages": [
"cmake"
],
"configurationAttributes": {
"launch": {
"properties": {
"scriptPath": {
"type": "string",
"descripttion": "%cmake-tools.debugger.scriptPath.description%",
"default": "script.cmake"
},
"scriptArgs": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "%cmake-tools.debugger.scriptArgs.description%"
},
"scriptEnv": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "%cmake-tools.debugger.name%"
},
"value": {
"type": "string",
"description": "%cmake-tools.debugger.value%"
}
}
},
"default": [],
"description": "%cmake-tools.debugger.scriptEnv.description%"
},
"dapLog": {
"type": "string",
"description": "%cmake-tools.debugger.dapLog.description%",
"default": ""
},
"pipeName": {
"type": "string",
"description": "%cmake-tools.debugger.pipeName.description%"
"description": "%cmake-tools.debugger.pipeName.description%",
"default": ""
},
"clean": {
"type": "boolean",
@ -737,14 +781,140 @@
"description": "%cmake-tools.debugger.configureAll.description%",
"default": false
},
"dapLog": {
"cmakeDebugType": {
"type": "string",
"description": "%cmake-tools.debugger.dapLog.description%",
"default": ""
}
"enum": ["configure", "external", "script"],
"description": "%cmake-tools.debugger.debugType.description%"
}
},
"required": [
"cmakeDebugType"
],
"oneOf": [
{
"properties": {
"cmakeDebugType": {
"enum": [
"script"
]
},
"scriptPath": {
"type": "string",
"description": "%cmake-tools.debugger.scriptPath.description%",
"default": ""
},
"scriptArgs": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "%cmake-tools.debugger.scriptArgs.description%"
},
"scriptEnv": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "%cmake-tools.debugger.name%"
},
"value": {
"type": "string",
"description": "%cmake-tools.debugger.value%"
}
}
},
"default": [],
"description": "%cmake-tools.debugger.scriptEnv.description%"
},
"dapLog": {
"type": "string",
"description": "%cmake-tools.debugger.dapLog.description%",
"default": ""
}
},
"required": [
"scriptPath"
]
},
{
"properties": {
"cmakeDebugType": {
"enum": [
"configure"
]
},
"clean": {
"type": "boolean",
"description": "%cmake-tools.debugger.clean.description%",
"default": false
},
"configureAll": {
"type": "boolean",
"description": "%cmake-tools.debugger.configureAll.description%",
"default": false
},
"dapLog": {
"type": "string",
"description": "%cmake-tools.debugger.dapLog.description%",
"default": ""
}
}
},
{
"properties": {
"cmakeDebugType": {
"enum": [
"external"
]
}
},
"required": [
"pipeName"
]
}
]
}
},
"initialConfigurations": [],
"configurationSnippets": [
{
"label": "%cmake-tools.debugger.configure.snippet.label%",
"description": "%cmake-tools.debugger.configure.snippet.description%",
"body": {
"type": "cmake",
"request": "launch",
"name": "%cmake-tools.debugger.configure.snippet.body.name%",
"cmakeDebugType": "configure",
"clean": false,
"configureAll": false
}
},
{
"label": "%cmake-tools.debugger.script.snippet.label%",
"description": "%cmake-tools.debugger.script.snippet.description%",
"body": {
"type": "cmake",
"request": "launch",
"name": "%cmake-tools.debugger.script.snippet.body.name%",
"cmakeDebugType": "script",
"scriptPath": "^\"\\${workspaceFolder}/<...>.cmake\""
}
},
{
"label": "%cmake-tools.debugger.external.snippet.label%",
"description": "%cmake-tools.debugger.external.snippet.description%",
"body": {
"type": "cmake",
"request": "launch",
"name": "%cmake-tools.debugger.external.snippet.body.name%",
"cmakeDebugType": "external",
"pipeName": "<...>"
}
}
}
]
}
],
"menus": {

Просмотреть файл

@ -205,6 +205,21 @@
"cmake-tools.debugger.clean.description": "Clean prior to configuring.",
"cmake-tools.debugger.configureAll.description": "Configure for all projects.",
"cmake-tools.debugger.dapLog.description": "Where the debugger DAP log should be logged.",
"cmake-tools.debugger.scriptPath.description": "The path to the script to debug.",
"cmake-tools.debugger.scriptArgs.description": "Arguments for the script to debug.",
"cmake-tools.debugger.scriptEnv.description": "Environment for the script to use.",
"cmake-tools.debugger.name": "Name",
"cmake-tools.debugger.value": "Value",
"cmake-tools.debugger.debugType.description": "The type of the CMake debug session. Available options are: \"configure\", \"external\", \"script\".",
"cmake-tools.debugger.configure.snippet.label": "CMake: Configure",
"cmake-tools.debugger.configure.snippet.description": "Debug a CMake project configuration",
"cmake-tools.debugger.configure.snippet.body.name": "CMake: Configure project",
"cmake-tools.debugger.script.snippet.label": "CMake: Script",
"cmake-tools.debugger.script.snippet.description": "Debug a CMake script",
"cmake-tools.debugger.script.snippet.body.name": "CMake: Script debugging",
"cmake-tools.debugger.external.snippet.label": "CMake: External",
"cmake-tools.debugger.external.snippet.description": "Connect to an externally launched CMake invocation",
"cmake-tools.debugger.external.snippet.body.name": "CMake: Externally launched",
"cmake-tools.taskDefinitions.properties.label.description": "The name of the task",
"cmake-tools.taskDefinitions.properties.command.description": "CMake command",
"cmake-tools.taskDefinitions.properties.targets.description": "CMake build targets",

Просмотреть файл

@ -47,7 +47,7 @@ import { PresetsController } from './presetsController';
import paths from './paths';
import { ProjectController } from './projectController';
import { MessageItem } from 'vscode';
import { DebugTrackerFactory, DebuggerInformation } from './debug/debuggerConfigureDriver';
import { DebugTrackerFactory, DebuggerInformation, getDebuggerPipeName } from './debug/debuggerConfigureDriver';
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
@ -1392,7 +1392,9 @@ export class CMakeProject {
{title: localize('no.configureWithDebugger.button', 'Cancel')})
.then(async chosen => {
if (chosen && chosen.title === yesButtonTitle) {
await this.configureInternal(trigger, extraArgs, ConfigureType.NormalWithDebugger);
await this.configureInternal(trigger, extraArgs, ConfigureType.NormalWithDebugger, {
pipeName: getDebuggerPipeName()
});
}
});
}

Просмотреть файл

@ -1,10 +1,19 @@
import { extensionManager } from "@cmt/extension";
import * as vscode from "vscode";
import { DebuggerInformation, getDebuggerPipeName } from "./debuggerConfigureDriver";
import { executeScriptWithDebugger } from "./debuggerScriptDriver";
import * as logging from '../logging';
import * as nls from "vscode-nls";
import { fs } from "../pr";
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
const logger = logging.createLogger('debugger');
export class DebugAdapterNamedPipeServerDescriptorFactory implements vscode.DebugAdapterDescriptorFactory {
async createDebugAdapterDescriptor(session: vscode.DebugSession, _executable: vscode.DebugAdapterExecutable | undefined): Promise<vscode.ProviderResult<vscode.DebugAdapterDescriptor>> {
// first invoke cmake
// invoke internal methods that call into and maybe have a handler once we've got the debugger is ready
const pipeName = session.configuration.pipeName ?? getDebuggerPipeName();
const debuggerInformation: DebuggerInformation = {
pipeName,
@ -15,35 +24,49 @@ export class DebugAdapterNamedPipeServerDescriptorFactory implements vscode.Debu
// undocumented configuration field that lets us know if the session is being invoked from a command
// This should only be used from inside the extension from a command that invokes the debugger.
if (!session.configuration.fromCommand) {
const promise = new Promise<void>((resolve) => {
debuggerInformation.debuggerIsReady = resolve;
});
const cmakeDebugType: "configure" | "script" | "external" = session.configuration.cmakeDebugType;
if (cmakeDebugType === "configure" || cmakeDebugType === "script") {
const promise = new Promise<void>((resolve) => {
debuggerInformation.debuggerIsReady = resolve;
});
if (session.configuration.clean) {
if (session.configuration.configureAll) {
void extensionManager?.cleanConfigureAllWithDebuggerInternal(
debuggerInformation
);
if (cmakeDebugType === "script") {
const script = session.configuration.scriptPath;
if (!fs.existsSync(script)) {
throw new Error(localize("cmake.debug.scriptPath.does.not.exist", "The script path, \"{0}\", could not be found.", script));
}
const args: string[] = session.configuration.scriptArgs ?? [];
const env = new Map<string, string>(session.configuration.scriptEnv?.map((e: {name: string; value: string}) => [e.name, e.value])) ?? new Map();
void executeScriptWithDebugger(script, args, env, debuggerInformation);
} else {
void extensionManager?.cleanConfigureWithDebuggerInternal(
debuggerInformation
);
}
} else {
if (session.configuration.configureAll) {
void extensionManager?.configureAllWithDebuggerInternal(
debuggerInformation
);
} else {
void extensionManager?.configureWithDebuggerInternal(
debuggerInformation
);
if (session.configuration.clean) {
if (session.configuration.configureAll) {
void extensionManager?.cleanConfigureAllWithDebuggerInternal(
debuggerInformation
);
} else {
void extensionManager?.cleanConfigureWithDebuggerInternal(
debuggerInformation
);
}
} else {
if (session.configuration.configureAll) {
void extensionManager?.configureAllWithDebuggerInternal(
debuggerInformation
);
} else {
void extensionManager?.configureWithDebuggerInternal(
debuggerInformation
);
}
}
}
await promise;
}
await promise;
}
logger.info(localize('debugger.create.descriptor', 'Connecting debugger on named pipe: \"{0}\"', pipeName));
return new vscode.DebugAdapterNamedPipeServer(pipeName);
}
}

Просмотреть файл

@ -0,0 +1,76 @@
import * as vscode from "vscode";
import * as nls from "vscode-nls";
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
export class DynamicDebugConfigurationProvider implements vscode.DebugConfigurationProvider {
provideDebugConfigurations(_folder: vscode.WorkspaceFolder | undefined, _token?: vscode.CancellationToken | undefined): vscode.ProviderResult<vscode.DebugConfiguration[]> {
const providers: vscode.DebugConfiguration[] = [];
providers.push(
{
name: 'CMake: CMake Script',
type: "cmake",
request: "launch",
cmakeDebugType: "script",
scriptPath: '${file}'
}
);
return providers;
}
}
export class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
resolveDebugConfiguration(_folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration, _token?: vscode.CancellationToken | undefined): vscode.ProviderResult<vscode.DebugConfiguration> {
if (!debugConfiguration.type && !debugConfiguration.request && !debugConfiguration.name) {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.fileName.endsWith(".cmake")) {
debugConfiguration.type = "cmake";
debugConfiguration.name = localize("cmake.debug.without.launch", "Debugging cmake script with default launch");
debugConfiguration.request = "launch";
debugConfiguration.cmakeDebugType = "script";
debugConfiguration.scriptPath = editor.document.fileName;
} else {
throw new Error (localize("cmake.debugging.not.supported", "CMake does not support automatic debugging for this file"));
}
}
if (debugConfiguration.request !== "launch") {
throw new Error(
localize(
"cmake.debug.only.launch.supported",
'The "cmake" debug type only supports the "launch" request.'
)
);
}
if (debugConfiguration.cmakeDebugType === undefined) {
throw new Error(
localize(
"cmake.debug.must.define.debugType",
'The "cmake" debug type requires you to define the "cmakeDebugType". Available options are "configure", "external", and "script".'
)
);
} else {
if (debugConfiguration.cmakeDebugType === "external" && debugConfiguration.pipeName === undefined) {
throw new Error(
localize(
"cmake.debug.external.requires.pipeName",
'The "cmake" debug type with "cmakeDebugType" set to "external" requires you to define "pipeName".'
)
);
} else if (debugConfiguration.cmakeDebugType === "script" && !(debugConfiguration.scriptPath.endsWith(".cmake") || (debugConfiguration.scriptPath === "${file}" && vscode.window.activeTextEditor?.document.fileName.endsWith(".cmake")))) {
throw new Error(
localize(
"cmake.debug.script.requires.scriptPath",
'The "cmake" debug type with "cmakeDebugType" set to "script" requires you to define a "scriptPath" that points to a CMake script.'
)
);
}
}
return debugConfiguration;
}
}

Просмотреть файл

@ -0,0 +1,64 @@
import { CMakeOutputConsumer, StateMessage } from '@cmt/diagnostics/cmake';
import * as proc from '@cmt/proc';
import { DebuggerInformation } from './debuggerConfigureDriver';
import { getCMakeExecutableInformation } from '@cmt/cmake/cmakeExecutable';
import { extensionManager } from '@cmt/extension';
import * as logging from '../logging';
import * as nls from "vscode-nls";
import { EnvironmentUtils } from '@cmt/environmentVariables';
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
const cmakeLogger = logging.createLogger('cmake');
const scriptLogger = logging.createLogger('cmake-script');
export async function executeScriptWithDebugger(scriptPath: string, scriptArgs: string[], scriptEnv: Map<string, string>, debuggerInformation: DebuggerInformation): Promise<void> {
const outputConsumer: CMakeOutputConsumer = new CMakeOutputConsumer("", scriptLogger);
// This is dependent on there being an active project. This feels reasonable since we're expecting them to be in a CMake project.
// However, it could be safer to simply grab the cmake path directly from the settings.
const cmakeProject = extensionManager?.getActiveProject();
const cmakePath = await cmakeProject?.getCMakePathofProject();
if (cmakeProject && cmakePath) {
const cmakeExe = await getCMakeExecutableInformation(cmakePath);
if (cmakeExe.isDebuggerSupported) {
const concreteArgs = ["-P", scriptPath];
concreteArgs.push(...scriptArgs);
concreteArgs.push("--debugger");
concreteArgs.push("--debugger-pipe");
concreteArgs.push(`${debuggerInformation.pipeName}`);
if (debuggerInformation.dapLog) {
concreteArgs.push("--debugger-dap-log");
concreteArgs.push(debuggerInformation.dapLog);
}
cmakeLogger.info(localize('run.script', "Executing CMake script: \"{0}\"", scriptPath));
const env = EnvironmentUtils.merge([process.env, EnvironmentUtils.create(scriptEnv)]);
const child = proc.execute(cmakeExe.path, concreteArgs, outputConsumer, { environment: env});
while (
!outputConsumer.stateMessages.includes(
StateMessage.WaitingForDebuggerClient
)
) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
if (debuggerInformation.debuggerIsReady) {
debuggerInformation.debuggerIsReady();
}
const result = await child.result;
if (result.retc === 0) {
cmakeLogger.info(localize('run.script.successful', "CMake script: \"{0}\" completed successfully.", scriptPath));
} else {
cmakeLogger.info(localize('run.script.failed', "CMake script: \"{0}\" completed unsuccessfully.", scriptPath));
throw new Error("HEY");
}
} else {
cmakeLogger.error(localize('run.script.cmakeDebugger.not.supported', "Cannot debug a script with this version of CMake, ensure you have CMake version 3.27 or later."));
}
}
}

Просмотреть файл

@ -279,6 +279,7 @@ export class CMakeFileApiDriver extends CMakeDriver {
name: localize("cmake.debug.name", "CMake Debugger"),
request: "launch",
type: "cmake",
cmakeDebugType: "configure",
pipeName: debuggerInformation.pipeName,
fromCommand: true
});

Просмотреть файл

@ -45,6 +45,7 @@ import { StatusBar } from '@cmt/status';
import { DebugAdapterNamedPipeServerDescriptorFactory } from './debug/debugAdapterNamedPipeServerDescriptorFactory';
import { getCMakeExecutableInformation } from './cmake/cmakeExecutable';
import { DebuggerInformation, getDebuggerPipeName } from './debug/debuggerConfigureDriver';
import { DebugConfigurationProvider, DynamicDebugConfigurationProvider } from './debug/debugConfigurationProvider';
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
@ -1758,6 +1759,14 @@ async function setup(context: vscode.ExtensionContext, progress?: ProgressHandle
)
);
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("cmake", new DebugConfigurationProvider()));
context.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider(
"cmake",
new DynamicDebugConfigurationProvider(),
vscode.DebugConfigurationProviderTriggerKind.Dynamic)
);
// List of functions that will be bound commands
const funs: (keyof ExtensionManager)[] = [
'activeFolderName',