import * as debugUtils from './utils' ; import * as os from 'os' ; import * as path from 'path' ; import * as vscode from 'vscode' ; import { CppBuildTask , CppBuildTaskDefinition } from '../LanguageServer/cppBuildTaskProvider' ; import * as util from '../common' ; import * as fs from 'fs' ; import * as Telemetry from '../telemetry' ; import { buildAndDebugActiveFileStr } from './extension' ; import { cppBuildTaskProvider } from '../LanguageServer/extension' ; import * as logger from '../logger' ; import * as nls from 'vscode-nls' ; import { IConfiguration , IConfigurationSnippet , DebuggerType , MIConfigurations , WindowsConfigurations , WSLConfigurations , PipeTransportConfigurations } from './configurations' ; import { parse } from 'comment-json' ; import { PlatformInformation } from '../platform' ; import { Environment , ParsedEnvironmentFile } from './ParsedEnvironmentFile' ; nls . config ( { messageFormat : nls . MessageFormat . bundle , bundleFormat : nls . BundleFormat . standalone } ) ( ) ; const localize = nls . loadMessageBundle ( ) ; function isDebugLaunchStr ( str ) { return str . startsWith ( "(gdb) " ) || str . startsWith ( "(lldb) " ) || str . startsWith ( "(Windows) " ) ; } export class QuickPickConfigurationProvider implements vscode . DebugConfigurationProvider { private underlyingProvider : vscode . DebugConfigurationProvider ; public constructor ( provider ) { this . underlyingProvider = provider ; } async provideDebugConfigurations ( folder ? , token ? ) { let configs = this . underlyingProvider . provideDebugConfigurations ? await this . underlyingProvider . provideDebugConfigurations ( folder , token ) : undefined ; if ( ! configs ) { configs = [ ] ; } const defaultConfig = configs . find ( config => isDebugLaunchStr ( config . name ) && config . request === "launch" ) ; if ( ! defaultConfig ) { throw new Error ( "Default config not found in provideDebugConfigurations()" ) ; } const editor = vscode . window . activeTextEditor ; if ( ! editor || ! util . fileIsCOrCppSource ( editor . document . fileName ) || configs . length <= 1 ) { return [ defaultConfig ] ; } interface MenuItem extends vscode . QuickPickItem { configuration : vscode . DebugConfiguration ; } const items = configs . map < MenuItem > ( config => { const noDetailConfig = { ... config } ; noDetailConfig . detail = undefined ; const menuItem = { label : config . name , configuration : noDetailConfig , description : config . detail } ; if ( isDebugLaunchStr ( menuItem . label ) ) { menuItem . label = localize ( "default.configuration.menuitem" , "Default Configuration" ) ; } return menuItem ; } ) ; const selection = await vscode . window . showQuickPick ( items , { placeHolder : localize ( "select.configuration" , "Select a configuration" ) } ) ; if ( ! selection ) { throw Error ( localize ( "debug.configuration.selection.canceled" , "Debug configuration selection canceled" ) ) ; } if ( selection . label . startsWith ( "cl.exe" ) ) { if ( ! process . env . DevEnvDir ) { vscode . window . showErrorMessage ( localize ( "cl.exe.not.available" , "{0} build and debug is only usable when VS Code is run from the Developer Command Prompt for VS." , "cl.exe" ) ) ; return [ selection . configuration ] ; } } if ( selection . label . indexOf ( buildAndDebugActiveFileStr ( ) ) !== - 1 && selection . configuration . preLaunchTask ) { try { await cppBuildTaskProvider . ensureBuildTaskExists ( selection . configuration . preLaunchTask ) ; if ( selection . configuration . miDebuggerPath ) { if ( ! await util . checkFileExists ( selection . configuration . miDebuggerPath ) ) { vscode . window . showErrorMessage ( localize ( "miDebuggerPath.not.available" , "miDebuggerPath does not exist: {0}. Has a debugger been installed?" , selection . configuration . miDebuggerPath ) ) ; throw new Error ( ) ; } } await vscode . debug . startDebugging ( folder , selection . configuration ) ; Telemetry . logDebuggerEvent ( "buildAndDebug" , { "success" : "true" } ) ; } catch ( e ) { Telemetry . logDebuggerEvent ( "buildAndDebug" , { "success" : "false" } ) ; } } return [ selection . configuration ] ; } resolveDebugConfiguration ( folder , config , token ? ) { return this . underlyingProvider . resolveDebugConfiguration ? this . underlyingProvider . resolveDebugConfiguration ( folder , config , token ) : undefined ; } resolveDebugConfigurationWithSubstitutedVariables ( folder , config , token ? ) { return this . underlyingProvider . resolveDebugConfigurationWithSubstitutedVariables ? this . underlyingProvider . resolveDebugConfigurationWithSubstitutedVariables ( folder , config , token ) : undefined ; } } class CppConfigurationProvider implements vscode . DebugConfigurationProvider { private type : DebuggerType ; private provider : IConfigurationAssetProvider ; public constructor ( provider , type ) { this . provider = provider ; this . type = type ; } /**
     * Returns a list of initial debug configurations based on contextual information, e.g. package.json or folder.
     */ async provideDebugConfigurations ( folder ? , token ? ) { const defaultConfig = this . provider . getInitialConfigurations ( this . type ) . find ( ( config ) => isDebugLaunchStr ( config . name ) && config . request === "launch" ) ; console . assert ( defaultConfig , "Could not find default debug configuration." ) ; const platformInfo = await PlatformInformation . GetPlatformInformation ( ) ; const platform = platformInfo . platform ; const buildTasksJson = await cppBuildTaskProvider . getJsonTasks ( ) ; const buildTasksDetected = await cppBuildTaskProvider . getTasks ( true ) ; const buildTasksDetectedRename = buildTasksDetected . map ( taskDetected => { for ( const taskJson of buildTasksJson ) { if ( ( taskDetected . definition . label as string ) === ( taskJson . definition . label as string ) ) { taskDetected . name = cppBuildTaskProvider . provideUniqueTaskLabel ( taskJson . definition . label , buildTasksJson ) ; taskDetected . definition . label = taskDetected . name ; break ; } } return taskDetected ; } ) ; let buildTasks = [ ] ; buildTasks = buildTasks . concat ( buildTasksJson , buildTasksDetectedRename ) ; if ( buildTasks . length === 0 ) { return Promise . resolve ( this . provider . getInitialConfigurations ( this . type ) ) ; } if ( buildTasks . length === 0 ) { return Promise . resolve ( this . provider . getInitialConfigurations ( this . type ) ) ; } buildTasks = buildTasks . filter ( ( task ) => { const command = task . definition . command as string ; if ( ! command ) { return false ; } if ( defaultConfig . name . startsWith ( "(Windows) " ) ) { if ( command . startsWith ( "cl.exe" ) ) { return true ; } } else { if ( ! command . startsWith ( "cl.exe" ) ) { return true ; } } return false ; } ) ; const configs = await Promise . all ( buildTasks . map < Promise < vscode . DebugConfiguration > > ( async ( task ) => { const definition = task . definition as CppBuildTaskDefinition ; const compilerPath = definition . command ; const compilerName = path . basename ( compilerPath ) ; const newConfig = { ... defaultConfig } ; newConfig . name = compilerName + buildAndDebugActiveFileStr ( ) ; newConfig . preLaunchTask = task . name ; if ( newConfig . type === "cppdbg" ) { newConfig . externalConsole = false ; } else { newConfig . console = "externalTerminal" ; } const exeName = path . join ( "${fileDirname}" , "${fileBasenameNoExtension}" ) ; const isWindows = platform === 'win32' ; newConfig . program = isWindows ? exeName + ".exe" : exeName ; newConfig . detail = task . detail ? task . detail : definition . command ; const isCl = compilerName === "cl.exe" ; newConfig . cwd = isWindows && ! isCl && ! process . env . PATH ?. includes ( path . dirname ( compilerPath ) ) ? path . dirname ( compilerPath ) : "${fileDirname}" ; return new Promise < vscode . DebugConfiguration > ( resolve => { if ( platform === "darwin" ) { return resolve ( newConfig ) ; } else { let debuggerName ; if ( compilerName . startsWith ( "clang" ) ) { newConfig . MIMode = "lldb" ; debuggerName = "lldb-mi" ; if ( ( compilerName !== "clang-cl.exe" ) && ( compilerName !== "clang-cpp.exe" ) ) { const suffixIndex = compilerName . indexOf ( "-" ) ; if ( suffixIndex !== - 1 ) { const suffix = compilerName . substr ( suffixIndex ) ; debuggerName += suffix ; } } newConfig . type = "cppdbg" ; } else if ( compilerName === "cl.exe" ) { newConfig . miDebuggerPath = undefined ; newConfig . type = "cppvsdbg" ; return resolve ( newConfig ) ; } else { debuggerName = "gdb" ; } if ( isWindows ) { debuggerName = debuggerName . endsWith ( ".exe" ) ? debuggerName : ( debuggerName + ".exe" ) ; } const compilerDirname = path . dirname ( compilerPath ) ; const debuggerPath = path . join ( compilerDirname , debuggerName ) ; if ( isWindows ) { newConfig . miDebuggerPath = debuggerPath ; return resolve ( newConfig ) ; } else { fs . stat ( debuggerPath , ( err , stats ) => { if ( ! err && stats && stats . isFile ) { newConfig . miDebuggerPath = debuggerPath ; } else { newConfig . miDebuggerPath = path . join ( "/usr" , "bin" , debuggerName ) ; } return resolve ( newConfig ) ; } ) ; } } } ) ; } ) ) ; configs . push ( defaultConfig ) ; return configs ; } /**
     * Error checks the provided 'config' without any variables substituted.
     */ resolveDebugConfiguration ( folder , config , token ? ) { if ( ! config || ! config . type ) { return null ; } if ( config . type === 'cppvsdbg' ) { if ( config . hasOwnProperty ( "externalConsole" ) ) { logger . getOutputChannelLogger ( ) . showWarningMessage ( localize ( "debugger.deprecated.config" , "The key '{0}' is deprecated. Please use '{1}' instead." , "externalConsole" , "console" ) ) ; if ( config . externalConsole && ! config . console ) { config . console = "externalTerminal" ; } delete config . externalConsole ; } if ( os . platform ( ) !== 'win32' ) { 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" ) ) ; return undefined ; } } return config ; } /**
     * This hook is directly called after 'resolveDebugConfiguration' but with all variables substituted.
     * This is also ran after the tasks.json has completed.
     *
     * Try to add all missing attributes to the debug configuration being launched.
     */ resolveDebugConfigurationWithSubstitutedVariables ( folder , config , token ? ) { if ( ! config || ! config . type ) { return null ; } if ( config . type === 'cppvsdbg' ) { if ( ! config . enableDebugHeap ) { const disableDebugHeapEnvSetting = { "name" : "_NO_DEBUG_HEAP" , "value" : "1" } ; if ( config . environment && util . isArray ( config . environment ) ) { config . environment . push ( disableDebugHeapEnvSetting ) ; } else { config . environment = [ disableDebugHeapEnvSetting ] ; } } } this . resolveEnvFile ( config , folder ) ; this . resolveSourceFileMapVariables ( config ) ; if ( os . platform ( ) === 'win32' && config . pipeTransport && config . pipeTransport . pipeProgram ) { let replacedPipeProgram ; const pipeProgramStr = config . pipeTransport . pipeProgram . toLowerCase ( ) . trim ( ) ; replacedPipeProgram = debugUtils . ArchitectureReplacer . checkAndReplaceWSLPipeProgram ( pipeProgramStr , debugUtils . ArchType . ia32 ) ; if ( ! replacedPipeProgram && ! path . isAbsolute ( pipeProgramStr ) && config . pipeTransport . pipeCwd ) { const pipeCwdStr = config . pipeTransport . pipeCwd . toLowerCase ( ) . trim ( ) ; const newPipeProgramStr = path . join ( pipeCwdStr , pipeProgramStr ) ; replacedPipeProgram = debugUtils . ArchitectureReplacer . checkAndReplaceWSLPipeProgram ( newPipeProgramStr , debugUtils . ArchType . ia32 ) ; } if ( replacedPipeProgram ) { config . pipeTransport . pipeProgram = replacedPipeProgram ; } } const macOSMIMode = config . osx ?. MIMode ?? config . MIMode ; const macOSMIDebuggerPath = config . osx ?. miDebuggerPath ?? config . miDebuggerPath ; const lldb_mi_10_x_path = path . join ( util . extensionPath , "debugAdapters" , "lldb-mi" , "bin" , "lldb-mi" ) ; if ( os . platform ( ) === 'darwin' && fs . existsSync ( lldb_mi_10_x_path ) && ( ! macOSMIMode || macOSMIMode === 'lldb' ) && ! macOSMIDebuggerPath ) { const frameworkPath = this . getLLDBFrameworkPath ( ) ; if ( ! frameworkPath ) { const moreInfoButton = localize ( "lldb.framework.install.xcode" , "More Info" ) ; const LLDBFrameworkMissingMessage = localize ( "lldb.framework.not.found" , "Unable to locate 'LLDB.framework' for lldb-mi. Please install XCode or XCode Command Line Tools." ) ; vscode . window . showErrorMessage ( LLDBFrameworkMissingMessage , moreInfoButton ) . then ( value => { if ( value === moreInfoButton ) { const helpURL = "https://aka.ms/vscode-cpptools/LLDBFrameworkNotFound" ; vscode . env . openExternal ( vscode . Uri . parse ( helpURL ) ) ; } } ) ; return undefined ; } } if ( config . logging ?. engineLogging ) { const outputChannel = logger . getOutputChannelLogger ( ) ; outputChannel . appendLine ( localize ( "debugger.launchConfig" , "Launch configuration:" ) ) ; outputChannel . appendLine ( JSON . stringify ( config , undefined , 2 ) ) ; } return config ; } private getLLDBFrameworkPath ( ) { const LLDBFramework = "LLDB.framework" ; const searchPaths = [ "/Library/Developer/CommandLineTools/Library/PrivateFrameworks" , "/Applications/Xcode.app/Contents/SharedFrameworks" ] ; for ( const searchPath of searchPaths ) { if ( fs . existsSync ( path . join ( searchPath , LLDBFramework ) ) ) { return searchPath ; } } const outputChannel = logger . getOutputChannelLogger ( ) ; outputChannel . appendLine ( localize ( "lldb.find.failed" , "Missing dependency '{0}' for lldb-mi executable." , LLDBFramework ) ) ; outputChannel . appendLine ( localize ( "lldb.search.paths" , "Searched in:" ) ) ; searchPaths . forEach ( searchPath => { outputChannel . appendLine ( `\t${ searchPath }` ) ; } ) ; const xcodeCLIInstallCmd = "xcode-select --install" ; outputChannel . appendLine ( localize ( "lldb.install.help" , "To resolve this issue, either install XCode through the Apple App Store or install the XCode Command Line Tools by running '{0}' in a Terminal window." , xcodeCLIInstallCmd ) ) ; logger . showOutputChannel ( ) ; return undefined ; } private resolveEnvFile ( config , folder ? ) { if ( config . envFile ) { let envFilePath = util . resolveVariables ( config . envFile , undefined ) ; try { if ( folder && folder . uri && folder . uri . fsPath ) { envFilePath = envFilePath . replace ( /(\${workspaceFolder}|\${workspaceRoot})/g , folder . uri . fsPath ) ; } const parsedFile = ParsedEnvironmentFile . CreateFromFile ( envFilePath , config [ "environment" ] ) ; if ( parsedFile . Warning ) { CppConfigurationProvider . showFileWarningAsync ( parsedFile . Warning , config . envFile ) ; } config . environment = parsedFile . Env ; delete config . envFile ; } catch ( errJS ) { const e = errJS as Error ; throw new Error ( localize ( "envfale.failed" , "Failed to use {0}. Reason: {1}" , "envFile" , e . message ) ) ; } } } private resolveSourceFileMapVariables ( config ) { const messages = [ ] ; if ( config . sourceFileMap ) { for ( const sourceFileMapSource of Object . keys ( config . sourceFileMap ) ) { let message = "" ; const sourceFileMapTarget = config . sourceFileMap [ sourceFileMapSource ] ; let source = sourceFileMapSource ; let target = sourceFileMapTarget ; const newSourceFileMapSource = util . resolveVariables ( sourceFileMapSource , undefined ) ; if ( sourceFileMapSource !== newSourceFileMapSource ) { message = "\t" + localize ( "replacing.sourcepath" , "Replacing {0} '{1}' with '{2}'." , "sourcePath" , sourceFileMapSource , newSourceFileMapSource ) ; delete config . sourceFileMap [ sourceFileMapSource ] ; source = newSourceFileMapSource ; } if ( util . isString ( sourceFileMapTarget ) ) { const newSourceFileMapTarget = util . resolveVariables ( sourceFileMapTarget , undefined ) ; if ( sourceFileMapTarget !== newSourceFileMapTarget ) { message += ( message ? ' ' : '\t' ) ; message += localize ( "replacing.targetpath" , "Replacing {0} '{1}' with '{2}'." , "targetPath" , sourceFileMapTarget , newSourceFileMapTarget ) ; target = newSourceFileMapTarget ; } } else if ( util . isObject ( sourceFileMapTarget ) ) { const newSourceFileMapTarget = sourceFileMapTarget ; newSourceFileMapTarget [ "editorPath" ] = util . resolveVariables ( sourceFileMapTarget [ "editorPath" ] , undefined ) ; if ( sourceFileMapTarget !== newSourceFileMapTarget ) { message += ( message ? ' ' : '\t' ) ; message += localize ( "replacing.editorPath" , "Replacing {0} '{1}' with '{2}'." , "editorPath" , sourceFileMapTarget , newSourceFileMapTarget [ "editorPath" ] ) ; target = newSourceFileMapTarget ; } } if ( message ) { config . sourceFileMap [ source ] = target ; messages . push ( message ) ; } } if ( messages . length > 0 ) { logger . getOutputChannel ( ) . appendLine ( localize ( "resolving.variables.in.sourcefilemap" , "Resolving variables in {0}..." , "sourceFileMap" ) ) ; messages . forEach ( ( message ) => { logger . getOutputChannel ( ) . appendLine ( message ) ; } ) ; logger . showOutputChannel ( ) ; } } } private static async showFileWarningAsync ( message , fileName ) { const openItem = { title : localize ( "open.envfile" , "Open {0}" , "envFile" ) } ; const result = await vscode . window . showWarningMessage ( message , openItem ) ; if ( result && result . title === openItem . title ) { const doc = await vscode . workspace . openTextDocument ( fileName ) ; if ( doc ) { vscode . window . showTextDocument ( doc ) ; } } } } export class CppVsDbgConfigurationProvider extends CppConfigurationProvider { public constructor ( provider ) { super ( provider , DebuggerType . cppvsdbg ) ; } } export class CppDbgConfigurationProvider extends CppConfigurationProvider { public constructor ( provider ) { super ( provider , DebuggerType . cppdbg ) ; } } export interface IConfigurationAssetProvider { getInitialConfigurations ( debuggerType ) : any ; getConfigurationSnippets ( ) : vscode . CompletionItem [ ] ; } export class ConfigurationAssetProviderFactory { public static getConfigurationProvider ( ) { switch ( os . platform ( ) ) { case 'win32' : return new WindowsConfigurationProvider ( ) ; case 'darwin' : return new OSXConfigurationProvider ( ) ; case 'linux' : return new LinuxConfigurationProvider ( ) ; default : throw new Error ( localize ( "unexpected.os" , "Unexpected OS type" ) ) ; } } } abstract class DefaultConfigurationProvider implements IConfigurationAssetProvider { configurations : IConfiguration [ ] = [ ] ; public getInitialConfigurations ( debuggerType ) { const configurationSnippet = [ ] ; this . configurations . forEach ( configuration => { configurationSnippet . push ( configuration . GetLaunchConfiguration ( ) ) ; } ) ; const initialConfigurations = configurationSnippet . filter ( snippet => snippet . debuggerType === debuggerType && snippet . isInitialConfiguration ) . map ( snippet => JSON . parse ( snippet . bodyText ) ) ; return initialConfigurations ; } public getConfigurationSnippets ( ) { const completionItems = [ ] ; this . configurations . forEach ( configuration => { completionItems . push ( convertConfigurationSnippetToCompetionItem ( configuration . GetLaunchConfiguration ( ) ) ) ; completionItems . push ( convertConfigurationSnippetToCompetionItem ( configuration . GetAttachConfiguration ( ) ) ) ; } ) ; return completionItems ; } } class WindowsConfigurationProvider extends DefaultConfigurationProvider { 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
    }
]` ; 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 ) ] ; } } class OSXConfigurationProvider extends DefaultConfigurationProvider { private MIMode : string = 'lldb' ; private executable : string = "a.out" ; private pipeProgram : string = "/usr/bin/ssh" ; constructor ( ) { super ( ) ; this . configurations = [ new MIConfigurations ( this . MIMode , this . executable , this . pipeProgram ) ] ; } } class LinuxConfigurationProvider extends DefaultConfigurationProvider { private MIMode : string = 'gdb' ; private setupCommandsBlock : string = `"setupCommands": [
    {
        "description": "${ localize ( "enable.pretty.printing" , "Enable pretty-printing for {0}" , "gdb" ) . replace ( /\"/g , "\\\"" ) }",
        "text": "-enable-pretty-printing",
        "ignoreFailures": true
    },
    {
        "description":  "${ localize ( "enable.intel.disassembly.flavor" , "Set Disassembly Flavor to {0}" , "Intel" ) . replace ( /\"/g , "\\\"" ) }",
        "text": "-gdb-set disassembly-flavor intel",
        "ignoreFailures": true
    }
]` ; private executable : string = "a.out" ; private pipeProgram : string = "/usr/bin/ssh" ; constructor ( ) { super ( ) ; this . configurations = [ new MIConfigurations ( this . MIMode , this . executable , this . pipeProgram , this . setupCommandsBlock ) , new PipeTransportConfigurations ( this . MIMode , this . executable , this . pipeProgram , this . setupCommandsBlock ) ] ; } } function convertConfigurationSnippetToCompetionItem ( snippet ) { const item = new vscode . CompletionItem ( snippet . label , vscode . CompletionItemKind . Snippet ) ; item . insertText = snippet . bodyText ; return item ; } export class ConfigurationSnippetProvider implements vscode . CompletionItemProvider { private provider : IConfigurationAssetProvider ; private snippets : vscode . CompletionItem [ ] ; constructor ( provider ) { this . provider = provider ; this . snippets = this . provider . getConfigurationSnippets ( ) ; } public resolveCompletionItem ( item , token ) { return Promise . resolve ( item ) ; } public provideCompletionItems ( document , position , token , context ) { let items = this . snippets ; const launch = parse ( document . getText ( ) ) ; if ( launch . configurations . length !== 0 ) { items = [ ] ; this . snippets . forEach ( ( item ) => items . push ( { ... item } ) ) ; items . map ( ( item ) => { item . insertText = item . insertText + ',' ; } ) ; } return Promise . resolve ( new vscode . CompletionList ( items , true ) ) ; } }