diff --git a/Extension/.scripts/generateOptionsSchema.ts b/Extension/.scripts/generateOptionsSchema.ts index b6c9a2029b..429561ea6e 100644 --- a/Extension/.scripts/generateOptionsSchema.ts +++ b/Extension/.scripts/generateOptionsSchema.ts @@ -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 diff --git a/Extension/package.json b/Extension/package.json index b97afbd9a1..1455b60229 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -58,6 +58,7 @@ "onCommand:extension.pickNativeProcess", "onCommand:extension.pickRemoteNativeProcess", "onDebugResolve:cppdbg", + "onDebugResolve:cpplldb", "onDebugResolve:cppvsdbg", "workspaceContains:/.vscode/c_cpp_properties.json", "onFileSystem:cpptools-schema" @@ -3652,7 +3653,7 @@ "program": { "type": "string", "description": "%c_cpp.debuggers.program.description%", - "default": "${workspaceRoot}/a.out" + "default": "${workspaceFolder}/a.out" }, "args": { "type": "array", @@ -4054,11 +4055,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 } } }, @@ -4603,7 +4602,7 @@ "program": { "type": "string", "description": "%c_cpp.debuggers.program.description%", - "default": "${workspaceRoot}/a.out" + "default": "${workspaceFolder}/a.out" }, "targetArchitecture": { "type": "string", @@ -4840,11 +4839,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 } } }, @@ -5381,7 +5378,6 @@ "label": "C++ (Windows)", "when": "workspacePlatform == windows", "languages": [ - "ada", "c", "cpp", "cuda-cpp", @@ -5403,7 +5399,7 @@ "program": { "type": "string", "description": "%c_cpp.debuggers.program.description%", - "default": "${workspaceRoot}/program.exe" + "default": "${workspaceFolder}/program.exe" }, "args": { "type": "array", @@ -5416,7 +5412,7 @@ "cwd": { "type": "string", "description": "%c_cpp.debuggers.cwd.description%", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "environment": { "type": "array", @@ -5622,6 +5618,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%", @@ -5764,6 +5765,1402 @@ } } } + }, + { + "type": "cpplldb", + "label": "C++ (LLDB-DAP)", + "languages": [ + "c", + "cpp", + "cuda-cpp", + "rust" + ], + "variables": { + "pickProcess": "extension.pickNativeProcess", + "pickRemoteProcess": "extension.pickRemoteNativeProcess" + }, + "configurationAttributes": { + "launch": { + "type": "object", + "required": [ + "program" + ], + "properties": { + "program": { + "type": "string", + "description": "%c_cpp.debuggers.program.description%", + "default": "${workspaceFolder}/a.out" + }, + "args": { + "type": "array", + "description": "%c_cpp.debuggers.args.description%", + "items": { + "type": "string" + }, + "default": [] + }, + "cwd": { + "type": "string", + "description": "%c_cpp.debuggers.cwd.description%", + "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": { + "type": "object", + "default": {}, + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "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": { + "": "" + } + }, + { + "type": "object", + "default": { + "": { + "editorPath": "", + "useForBreakpoints": true + } + }, + "properties": { + "": { + "type": "object", + "default": { + "editorPath": "", + "useForBreakpoints": true + }, + "properties": { + "editorPath": { + "type": "string", + "description": "%c_cpp.debuggers.sourceFileMap.sourceFileMapEntry.editorPath.description%", + "default": "" + }, + "useForBreakpoints": { + "type": "boolean", + "description": "%c_cpp.debuggers.sourceFileMap.sourceFileMapEntry.useForBreakpoints.description%", + "default": true + } + } + } + } + } + ] + }, + "deploySteps": { + "type": "array", + "description": "%c_cpp.debuggers.deploySteps.description%", + "items": { + "anyOf": [ + { + "type": "object", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", + "default": {}, + "required": [ + "type", + "files", + "host", + "targetDir" + ], + "properties": { + "type": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", + "default": "", + "enum": [ + "scp", + "rsync" + ] + }, + "files": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "%c_cpp.debuggers.deploySteps.copyFile.files.description%", + "default": "" + }, + "host": { + "anyOf": [ + { + "type": "string", + "description": "%c_cpp.debuggers.host.description%", + "default": "hello@microsoft.com" + }, + { + "type": "object", + "description": "%c_cpp.debuggers.host.description%", + "default": {}, + "required": [ + "hostName" + ], + "properties": { + "user": { + "type": "string", + "description": "%c_cpp.debuggers.host.user.description%", + "default": "" + }, + "hostName": { + "type": "string", + "description": "%c_cpp.debuggers.host.hostName.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.port.description%", + "default": 22 + }, + "jumpHosts": { + "type": "array", + "description": "%c_cpp.debuggers.host.jumpHost.description%", + "items": { + "type": "object", + "default": {}, + "required": [ + "hostName" + ], + "properties": { + "user": { + "type": "string", + "description": "%c_cpp.debuggers.host.user.description%", + "default": "" + }, + "hostName": { + "type": "string", + "description": "%c_cpp.debuggers.host.hostName.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.port.description%", + "default": 22 + } + } + } + }, + "localForwards": { + "type": "array", + "description": "%c_cpp.debuggers.host.localForward.description%", + "items": { + "type": "object", + "default": {}, + "properties": { + "bindAddress": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.bindAddress.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.localForward.port.description%" + }, + "host": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.host.description%", + "default": "" + }, + "hostPort": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.localForward.hostPort.description%" + }, + "localSocket": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.localSocket.description%", + "default": "" + }, + "remoteSocket": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.remoteSocket.description%", + "default": "" + } + } + } + } + } + } + ] + }, + "targetDir": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.targetDir.description%", + "default": "" + }, + "recursive": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.copyFile.recursive.description%", + "default": "true" + }, + "debug": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.debug%" + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "scp" + } + } + }, + "then": { + "properties": { + "scpPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.scpPath.description%", + "default": "" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rsync" + } + } + }, + "then": { + "properties": { + "rsyncPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.rsyncPath.description%", + "default": "" + } + } + } + } + ] + }, + { + "type": "object", + "description": "%c_cpp.debuggers.deploySteps.ssh.description%", + "default": {}, + "required": [ + "type", + "host", + "command" + ], + "properties": { + "type": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.ssh.description%", + "default": "", + "enum": [ + "ssh" + ] + }, + "host": { + "anyOf": [ + { + "type": "string", + "description": "%c_cpp.debuggers.host.description%", + "default": "hello@microsoft.com" + }, + { + "type": "object", + "description": "%c_cpp.debuggers.host.description%", + "default": {}, + "required": [ + "hostName" + ], + "properties": { + "user": { + "type": "string", + "description": "%c_cpp.debuggers.host.user.description%", + "default": "" + }, + "hostName": { + "type": "string", + "description": "%c_cpp.debuggers.host.hostName.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.port.description%", + "default": 22 + }, + "jumpHosts": { + "type": "array", + "description": "%c_cpp.debuggers.host.jumpHost.description%", + "items": { + "type": "object", + "default": {}, + "required": [ + "hostName" + ], + "properties": { + "user": { + "type": "string", + "description": "%c_cpp.debuggers.host.user.description%", + "default": "" + }, + "hostName": { + "type": "string", + "description": "%c_cpp.debuggers.host.hostName.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.port.description%", + "default": 22 + } + } + } + }, + "localForwards": { + "type": "array", + "description": "%c_cpp.debuggers.host.localForward.description%", + "items": { + "type": "object", + "default": {}, + "properties": { + "bindAddress": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.bindAddress.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.localForward.port.description%" + }, + "host": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.host.description%", + "default": "" + }, + "hostPort": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.localForward.hostPort.description%" + }, + "localSocket": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.localSocket.description%", + "default": "" + }, + "remoteSocket": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.remoteSocket.description%", + "default": "" + } + } + } + } + } + } + ] + }, + "command": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.ssh.command.description%", + "default": "" + }, + "sshPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.ssh.sshPath.description%", + "default": "" + }, + "continueOn": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.continueOn.description%", + "default": "" + }, + "debug": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.debug%" + } + } + }, + { + "type": "object", + "description": "%c_cpp.debuggers.deploySteps.shell.description%", + "default": {}, + "required": [ + "type", + "command" + ], + "properties": { + "type": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.shell.description%", + "default": "", + "enum": [ + "shell" + ] + }, + "command": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.shell.command.description%", + "default": "" + }, + "continueOn": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.continueOn.description%", + "default": "" + }, + "debug": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.debug%" + } + } + }, + { + "type": "object", + "description": "%c_cpp.debuggers.vsCodeCommand.description%", + "default": {}, + "required": [ + "type", + "command" + ], + "properties": { + "type": { + "type": "string", + "description": "%c_cpp.debuggers.vsCodeCommand.description%", + "default": "", + "enum": [ + "command" + ] + }, + "command": { + "type": "string", + "description": "%c_cpp.debuggers.vsCodeCommand.command.description%", + "default": "" + }, + "args": { + "type": "array", + "description": "%c_cpp.debuggers.vsCodeCommand.args.description%", + "items": { + "type": "string" + } + } + } + } + ] + }, + "default": [] + }, + "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": "" + } + } + }, + "attach": { + "type": "object", + "default": {}, + "properties": { + "program": { + "type": "string", + "description": "%c_cpp.debuggers.program.description%", + "default": "${workspaceFolder}/a.out" + }, + "debuggerPath": { + "type": "string", + "description": "%c_cpp.debuggers.cpplldb.debuggerPath.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": { + "": "" + } + }, + { + "type": "object", + "default": { + "": { + "editorPath": "", + "useForBreakpoints": true + } + }, + "properties": { + "": { + "type": "object", + "default": { + "editorPath": "", + "useForBreakpoints": true + }, + "properties": { + "editorPath": { + "type": "string", + "description": "%c_cpp.debuggers.sourceFileMap.sourceFileMapEntry.editorPath.description%", + "default": "" + }, + "useForBreakpoints": { + "type": "boolean", + "description": "%c_cpp.debuggers.sourceFileMap.sourceFileMapEntry.useForBreakpoints.description%", + "default": true + } + } + } + } + } + ] + }, + "serverLaunchTimeout": { + "type": "integer", + "description": "%c_cpp.debuggers.serverLaunchTimeout.description%", + "default": "10000" + }, + "waitFor": { + "type": "boolean", + "markdownDescription": "%c_cpp.debuggers.cpplldb.waitFor.markdownDescription%", + "default": true + }, + "deploySteps": { + "type": "array", + "description": "%c_cpp.debuggers.deploySteps.description%", + "items": { + "anyOf": [ + { + "type": "object", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", + "default": {}, + "required": [ + "type", + "files", + "host", + "targetDir" + ], + "properties": { + "type": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", + "default": "", + "enum": [ + "scp", + "rsync" + ] + }, + "files": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "%c_cpp.debuggers.deploySteps.copyFile.files.description%", + "default": "" + }, + "host": { + "anyOf": [ + { + "type": "string", + "description": "%c_cpp.debuggers.host.description%", + "default": "hello@microsoft.com" + }, + { + "type": "object", + "description": "%c_cpp.debuggers.host.description%", + "default": {}, + "required": [ + "hostName" + ], + "properties": { + "user": { + "type": "string", + "description": "%c_cpp.debuggers.host.user.description%", + "default": "" + }, + "hostName": { + "type": "string", + "description": "%c_cpp.debuggers.host.hostName.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.port.description%", + "default": 22 + }, + "jumpHosts": { + "type": "array", + "description": "%c_cpp.debuggers.host.jumpHost.description%", + "items": { + "type": "object", + "default": {}, + "required": [ + "hostName" + ], + "properties": { + "user": { + "type": "string", + "description": "%c_cpp.debuggers.host.user.description%", + "default": "" + }, + "hostName": { + "type": "string", + "description": "%c_cpp.debuggers.host.hostName.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.port.description%", + "default": 22 + } + } + } + }, + "localForwards": { + "type": "array", + "description": "%c_cpp.debuggers.host.localForward.description%", + "items": { + "type": "object", + "default": {}, + "properties": { + "bindAddress": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.bindAddress.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.localForward.port.description%" + }, + "host": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.host.description%", + "default": "" + }, + "hostPort": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.localForward.hostPort.description%" + }, + "localSocket": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.localSocket.description%", + "default": "" + }, + "remoteSocket": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.remoteSocket.description%", + "default": "" + } + } + } + } + } + } + ] + }, + "targetDir": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.targetDir.description%", + "default": "" + }, + "recursive": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.copyFile.recursive.description%", + "default": "true" + }, + "debug": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.debug%" + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "scp" + } + } + }, + "then": { + "properties": { + "scpPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.scpPath.description%", + "default": "" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rsync" + } + } + }, + "then": { + "properties": { + "rsyncPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.rsyncPath.description%", + "default": "" + } + } + } + } + ] + }, + { + "type": "object", + "description": "%c_cpp.debuggers.deploySteps.ssh.description%", + "default": {}, + "required": [ + "type", + "host", + "command" + ], + "properties": { + "type": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.ssh.description%", + "default": "", + "enum": [ + "ssh" + ] + }, + "host": { + "anyOf": [ + { + "type": "string", + "description": "%c_cpp.debuggers.host.description%", + "default": "hello@microsoft.com" + }, + { + "type": "object", + "description": "%c_cpp.debuggers.host.description%", + "default": {}, + "required": [ + "hostName" + ], + "properties": { + "user": { + "type": "string", + "description": "%c_cpp.debuggers.host.user.description%", + "default": "" + }, + "hostName": { + "type": "string", + "description": "%c_cpp.debuggers.host.hostName.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.port.description%", + "default": 22 + }, + "jumpHosts": { + "type": "array", + "description": "%c_cpp.debuggers.host.jumpHost.description%", + "items": { + "type": "object", + "default": {}, + "required": [ + "hostName" + ], + "properties": { + "user": { + "type": "string", + "description": "%c_cpp.debuggers.host.user.description%", + "default": "" + }, + "hostName": { + "type": "string", + "description": "%c_cpp.debuggers.host.hostName.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.port.description%", + "default": 22 + } + } + } + }, + "localForwards": { + "type": "array", + "description": "%c_cpp.debuggers.host.localForward.description%", + "items": { + "type": "object", + "default": {}, + "properties": { + "bindAddress": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.bindAddress.description%", + "default": "" + }, + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.localForward.port.description%" + }, + "host": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.host.description%", + "default": "" + }, + "hostPort": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+$|^\\${.+}$" + } + ], + "description": "%c_cpp.debuggers.host.localForward.hostPort.description%" + }, + "localSocket": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.localSocket.description%", + "default": "" + }, + "remoteSocket": { + "type": "string", + "description": "%c_cpp.debuggers.host.localForward.remoteSocket.description%", + "default": "" + } + } + } + } + } + } + ] + }, + "command": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.ssh.command.description%", + "default": "" + }, + "sshPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.ssh.sshPath.description%", + "default": "" + }, + "continueOn": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.continueOn.description%", + "default": "" + }, + "debug": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.debug%" + } + } + }, + { + "type": "object", + "description": "%c_cpp.debuggers.deploySteps.shell.description%", + "default": {}, + "required": [ + "type", + "command" + ], + "properties": { + "type": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.shell.description%", + "default": "", + "enum": [ + "shell" + ] + }, + "command": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.shell.command.description%", + "default": "" + }, + "continueOn": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.continueOn.description%", + "default": "" + }, + "debug": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.debug%" + } + } + }, + { + "type": "object", + "description": "%c_cpp.debuggers.vsCodeCommand.description%", + "default": {}, + "required": [ + "type", + "command" + ], + "properties": { + "type": { + "type": "string", + "description": "%c_cpp.debuggers.vsCodeCommand.description%", + "default": "", + "enum": [ + "command" + ] + }, + "command": { + "type": "string", + "description": "%c_cpp.debuggers.vsCodeCommand.command.description%", + "default": "" + }, + "args": { + "type": "array", + "description": "%c_cpp.debuggers.vsCodeCommand.args.description%", + "items": { + "type": "string" + } + } + } + } + ] + }, + "default": [] + }, + "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": "" + } + } + } + } } ], "breakpoints": [ diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 2b729a732d..a39491c56c 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -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.", diff --git a/Extension/src/Debugger/attachQuickPick.ts b/Extension/src/Debugger/attachQuickPick.ts index f76d931e65..f4f07f5802 100644 --- a/Extension/src/Debugger/attachQuickPick.ts +++ b/Extension/src/Debugger/attachQuickPick.ts @@ -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. diff --git a/Extension/src/Debugger/attachToProcess.ts b/Extension/src/Debugger/attachToProcess.ts index 4ff3ce0c2e..cb0e6907d5 100644 --- a/Extension/src/Debugger/attachToProcess.ts +++ b/Extension/src/Debugger/attachToProcess.ts @@ -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; } +/** + * 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 { 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 { + 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 { 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) { diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index f4f8745016..2c2b59ccb1 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -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[] = []; + + protected constructor() { + } -export class ConfigurationAssetProviderFactory { - public static getConfigurationProvider(): IConfigurationAssetProvider { + 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"; - private pipeProgram: string = "/usr/bin/ssh"; constructor() { super(); this.configurations = [ - new MIConfigurations(this.MIMode, this.executable, this.pipeProgram) + new LldbDapConfigurations(this.executable), + new MIConfigurations('lldb', this.executable) ]; } } -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 - } -]`; +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, 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 { diff --git a/Extension/src/Debugger/configurations.ts b/Extension/src/Debugger/configurations.ts index 96895c6da2..2d834ebb1a 100644 --- a/Extension/src/Debugger/configurations.ts +++ b/Extension/src/Debugger/configurations.ts @@ -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; // 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 { + 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 { + 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 { + 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 { + 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 = {}) { + 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 = {}) { + 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 = {}) { + 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 = {}) { + 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, '')}`; + 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, ''), + body: { + ...createRemoteAttachBlock(name, DebuggerType.cppdbg, this.executable), + ...createPipeTransportBlock(this.bashPipeProgram, this.MIMode, ["-c"]), + ...this.additionalProperties + }, + debuggerType: DebuggerType.cppdbg + }; + } +} - 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)}` : ""]); +/** Creates Configurations for an LLDB-DAP debugger */ +export class LldbDapConfigurations extends Configuration { + constructor(public executable: string, public additionalProperties: Record = {}) { + 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("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("attach.with", "Attach with {0}.", "LLDB-DAP").replace(/"/g, ''), + body: { + ...createAttachBlock(name, DebuggerType.cpplldb, this.executable), + ...this.additionalProperties + }, + debuggerType: DebuggerType.cpplldb }; } } diff --git a/Extension/src/Debugger/debugAdapterDescriptorFactory.ts b/Extension/src/Debugger/debugAdapterDescriptorFactory.ts index d43d71bc3c..42f378101b 100644 --- a/Extension/src/Debugger/debugAdapterDescriptorFactory.ts +++ b/Extension/src/Debugger/debugAdapterDescriptorFactory.ts @@ -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 { 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 { + + // 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]); + } +} diff --git a/Extension/src/Debugger/extension.ts b/Extension/src/Debugger/extension.ts index f784c13824..8fb192905d 100644 --- a/Extension/src/Debugger/extension.ts +++ b/Extension/src/Debugger/extension.ts @@ -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 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 | 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(); + 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}`); + } + } +} diff --git a/Extension/src/Debugger/lldb-dap.ts b/Extension/src/Debugger/lldb-dap.ts new file mode 100644 index 0000000000..6e0aecbc1b --- /dev/null +++ b/Extension/src/Debugger/lldb-dap.ts @@ -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":, "value": } objects. + // lldb-dap expects an object of { :, ... }. + 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; + } +} diff --git a/Extension/src/Debugger/nativeAttach.ts b/Extension/src/Debugger/nativeAttach.ts index 26914a90eb..627b9126f0 100644 --- a/Extension/src/Debugger/nativeAttach.ts +++ b/Extension/src/Debugger/nativeAttach.ts @@ -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; + } + + 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; + } - public toAttachItem(): AttachItem { - return { - label: this.name, - description: this.pid, - detail: this.commandLine, - id: 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 { - 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. + // whitespace(NUMBERS)whitespace(FIXED-WIDTH)whitespace(EVERYTHING-ELSE) + 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. + // whitespace(NUMBERS)whitespace(NOTWHITESPACE)whitespace(EVERYTHING-ELSE) + /^\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 ' '. + 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 ''. + if (executable === '' || cmdline.includes('')) { + 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 { 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); }); } } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 826c188146..36a99f7ebb 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -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; diff --git a/Extension/src/SSH/TargetsView/targetNodes.ts b/Extension/src/SSH/TargetsView/targetNodes.ts index 352083e908..271f9d8291 100644 --- a/Extension/src/SSH/TargetsView/targetNodes.ts +++ b/Extension/src/SSH/TargetsView/targetNodes.ts @@ -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 })(); diff --git a/Extension/src/SSH/commands.ts b/Extension/src/SSH/commands.ts index 159138f31a..c5435e5370 100644 --- a/Extension/src/SSH/commands.ts +++ b/Extension/src/SSH/commands.ts @@ -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'; diff --git a/Extension/src/SSH/sshCommandRunner.ts b/Extension/src/SSH/sshCommandRunner.ts index be60f83db3..8bb5ba6fd1 100644 --- a/Extension/src/SSH/sshCommandRunner.ts +++ b/Extension/src/SSH/sshCommandRunner.ts @@ -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 { diff --git a/Extension/src/Utility/Async/manualPromise.ts b/Extension/src/Utility/Async/manualPromise.ts index 2a93fe7eba..da04ffb8da 100644 --- a/Extension/src/Utility/Async/manualPromise.ts +++ b/Extension/src/Utility/Async/manualPromise.ts @@ -43,14 +43,14 @@ export class ManualPromise implements Promise { * A method to manually resolve the Promise. */ public resolve: (value?: T | PromiseLike | 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'; diff --git a/Extension/src/Utility/Remoting/snare.ts b/Extension/src/Utility/Remoting/snare.ts new file mode 100644 index 0000000000..84a018eb94 --- /dev/null +++ b/Extension/src/Utility/Remoting/snare.ts @@ -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. +/// + +// Polyfill `Symbol.dispose` +(Symbol as any).dispose ??= Symbol("Symbol.dispose"); +(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose"); + +const results = new Map>(); +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; + + /** + * 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(ctor: new (remote: RemoteConnection, instance: number) => T, instance: Promise): Promise; + + /** + * 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(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 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; + 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 | undefined; + let payload: Payload | undefined; + try { + if (ready) { + await ready; + } + result = new ManualPromise(); + 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: (ctor: new (remote: RemoteConnection, instance: number) => T, instance: number | Promise) => 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(); + +/** Maps objects to their [identity, reference count] pairs. */ +const instanceIndex = new Map(); + +/** + * 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(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): Promise; + +/** + * 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): Reference | undefined | Promise { + 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(), 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; +} + diff --git a/Extension/src/common-remote-safe.ts b/Extension/src/common-remote-safe.ts new file mode 100644 index 0000000000..b9b256c05b --- /dev/null +++ b/Extension/src/common-remote-safe.ts @@ -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) { + 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) { + 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): 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): 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 = (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; + +/** + * 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; +} + +/** 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): Promise { + 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 { + 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 { + 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 { + // 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 { + const result = new ManualPromise(); + + 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, maxDepth = 4, options: { result?: string; visited?: Set; topDepth?: number } = {}): Promise { + options.topDepth ??= maxDepth; + options.visited ??= new Set(); + 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(); + + // 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; +} diff --git a/Extension/src/common.ts b/Extension/src/common.ts index a48326eaea..d2c17a23e6 100644 --- a/Extension/src/common.ts +++ b/Extension/src/common.ts @@ -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 { - // 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 { - const result = new ManualPromise(); - - 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 { - if (!filePath) { return Promise.resolve(false); } - return new Promise(resolve => fs.access(filePath, permission, err => resolve(!err))); -} - -export function isExecutable(file: string): Promise { - return pathAccessible(file, fs.constants.X_OK); -} - export async function allowExecution(file: string): Promise { if (process.platform !== 'win32') { const exists: boolean = await checkFileExists(file); diff --git a/Extension/src/links.ts b/Extension/src/links.ts new file mode 100644 index 0000000000..c61e18d147 --- /dev/null +++ b/Extension/src/links.ts @@ -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"; diff --git a/Extension/src/localization.ts b/Extension/src/localization.ts new file mode 100644 index 0000000000..a0464edbaa --- /dev/null +++ b/Extension/src/localization.ts @@ -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(); diff --git a/Extension/src/logger.ts b/Extension/src/logger.ts index d14bb531f6..872363ae16 100644 --- a/Extension/src/logger.ts +++ b/Extension/src/logger.ts @@ -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); +} diff --git a/Extension/test/scenarios/Debugger/tests/integration.test.ts b/Extension/test/scenarios/Debugger/tests/integration.test.ts index 5ce790cbdb..b8715dc921 100644 --- a/Extension/test/scenarios/Debugger/tests/integration.test.ts +++ b/Extension/test/scenarios/Debugger/tests/integration.test.ts @@ -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 { + suiteSetup(async function (): Promise { const extension: vscode.Extension = 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?"); } diff --git a/Extension/test/scenarios/SingleRootProject/tests/extension.test.ts b/Extension/test/scenarios/SingleRootProject/tests/extension.test.ts index 62f06236a0..c3ee12620c 100644 --- a/Extension/test/scenarios/SingleRootProject/tests/extension.test.ts +++ b/Extension/test/scenarios/SingleRootProject/tests/extension.test.ts @@ -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); diff --git a/Extension/tools/OptionsSchema.json b/Extension/tools/OptionsSchema.json index 816b0800db..2b25cf54e4 100644 --- a/Extension/tools/OptionsSchema.json +++ b/Extension/tools/OptionsSchema.json @@ -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,6 +949,399 @@ } } }, + "CpplldbLaunchOptions": { + "type": "object", + "required": [ + "program" + ], + "properties": { + "program": { + "type": "string", + "description": "%c_cpp.debuggers.program.description%", + "default": "${workspaceFolder}/a.out" + }, + "args": { + "type": "array", + "description": "%c_cpp.debuggers.args.description%", + "items": { + "type": "string" + }, + "default": [] + }, + "cwd": { + "type": "string", + "description": "%c_cpp.debuggers.cwd.description%", + "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": { + "": "" + } + }, + { + "$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": { + "": "" + } + }, + { + "$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": [ @@ -961,7 +1352,7 @@ "program": { "type": "string", "description": "%c_cpp.debuggers.program.description%", - "default": "${workspaceRoot}/program.exe" + "default": "${workspaceFolder}/program.exe" }, "args": { "type": "array", @@ -974,7 +1365,7 @@ "cwd": { "type": "string", "description": "%c_cpp.debuggers.cwd.description%", - "default": "${workspaceRoot}" + "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%",