diff --git a/.github/workflows/ci_mac.yml b/.github/workflows/ci_mac.yml index 199629a7e2..7dfe198dd4 100644 --- a/.github/workflows/ci_mac.yml +++ b/.github/workflows/ci_mac.yml @@ -10,6 +10,6 @@ jobs: job: uses: ./.github/workflows/job-compile-and-test.yml with: - runner-env: macos-12 + runner-env: macos-14 platform: mac yarn-args: --network-timeout 100000 \ No newline at end of file diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index 067983feac..7e4bf4e896 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -1,1278 +1,1278 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - -import * as jsonc from 'comment-json'; -import * as fs from 'fs'; -import * as glob from 'glob'; -import * as os from 'os'; -import * as path from 'path'; -import { promisify } from 'util'; -import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; -import * as util from '../common'; -import { isWindows } from '../constants'; -import { expandAllStrings, ExpansionOptions, ExpansionVars } from '../expand'; -import { CppBuildTask, CppBuildTaskDefinition, cppBuildTaskProvider } from '../LanguageServer/cppBuildTaskProvider'; -import { configPrefix } from '../LanguageServer/extension'; -import { CppSettings, OtherSettings } from '../LanguageServer/settings'; -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 { NativeAttachItemsProviderFactory } from './nativeAttach'; -import { Environment, ParsedEnvironmentFile } from './ParsedEnvironmentFile'; -import * as debugUtils from './utils'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -enum StepType { - scp = 'scp', - rsync = 'rsync', - ssh = 'ssh', - shell = 'shell', - remoteShell = 'remoteShell', - command = 'command' -} - -const globAsync: (pattern: string, options?: glob.IOptions | undefined) => Promise = promisify(glob); - -/* - * Retrieves configurations from a provider and displays them in a quickpick menu to be selected. - * Ensures that the selected configuration's preLaunchTask (if existent) is populated in the user's task.json. - * Automatically starts debugging for "Build and Debug" configurations. - */ -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 static ClearDetectedBuildTasks(): void { - DebugConfigurationProvider.detectedCppBuildTasks = []; - DebugConfigurationProvider.detectedCBuildTasks = []; - } - - /** - * Returns a list of initial debug configurations based on contextual information, e.g. package.json or folder. - * resolveDebugConfiguration will be automatically called after this function. - */ - async provideDebugConfigurations(folder?: vscode.WorkspaceFolder, token?: vscode.CancellationToken): Promise { - let configs: CppDebugConfiguration[] | null | undefined = await this.provideDebugConfigurationsForType(this.type, folder, token); - if (!configs) { - configs = []; - } - const defaultTemplateConfig: CppDebugConfiguration | undefined = configs.find(config => isDebugLaunchStr(config.name) && config.request === "launch"); - if (!defaultTemplateConfig) { - throw new Error("Default config not found in provideDebugConfigurations()"); - } - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!editor || !util.isCppOrCFile(editor.document.uri) || configs.length <= 1) { - return [defaultTemplateConfig]; - } - - const defaultConfig: CppDebugConfiguration[] = this.findDefaultConfig(configs); - // If there was only one config defined for the default task, choose that config, otherwise ask the user to choose. - if (defaultConfig.length === 1) { - return defaultConfig; - } - - // Find the recently used task and place it at the top of quickpick list. - let recentlyUsedConfig: CppDebugConfiguration | undefined; - configs = configs.filter(config => { - if (config.taskStatus !== TaskStatus.recentlyUsed) { - return true; - } else { - recentlyUsedConfig = config; - return false; - } - }); - if (recentlyUsedConfig) { - configs.unshift(recentlyUsedConfig); - } - - const items: ConfigMenu[] = configs.map(config => { - const quickPickConfig: CppDebugConfiguration = { ...config }; - const menuItem: ConfigMenu = { label: config.name, configuration: quickPickConfig, description: config.detail, detail: config.taskStatus }; - // Rename the menu item for the default configuration as its name is non-descriptive. - if (isDebugLaunchStr(menuItem.label)) { - menuItem.label = localize("default.configuration.menuitem", "Default Configuration"); - } - return menuItem; - }); - - const selection: ConfigMenu | undefined = await vscode.window.showQuickPick(this.localizeConfigDetail(items), { placeHolder: localize("select.configuration", "Select a configuration") }); - if (!selection) { - Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": "debug", "configSource": ConfigSource.unknown, "configMode": ConfigMode.unknown, "cancelled": "true", "succeeded": "true" }); - return []; // User canceled it. - } - - if (this.isClConfiguration(selection.label)) { - this.showErrorIfClNotAvailable(selection.label); - } - - return [selection.configuration]; - } - - /** - * Error checks the provided 'config' without any variables substituted. - * If return "undefined", the debugging will be aborted silently. - * If return "null", the debugging will be aborted and launch.json will be opened. - * resolveDebugConfigurationWithSubstitutedVariables will be automatically called after this function. - */ - async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, _token?: vscode.CancellationToken): Promise { - if (!config || !config.type) { - // When DebugConfigurationProviderTriggerKind is Dynamic, this function will be called with an empty config. - // Hence, providing debug configs, and start debugging should be done manually. - // resolveDebugConfiguration will be automatically called after calling provideDebugConfigurations. - const configs: CppDebugConfiguration[] = await this.provideDebugConfigurations(folder); - if (!configs || configs.length === 0) { - Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile, "configMode": ConfigMode.noLaunchConfig, "cancelled": "true", "succeeded": "true" }); - return undefined; // aborts debugging silently - } else { - // Currently, we expect only one debug config to be selected. - console.assert(configs.length === 1, "More than one debug config is selected."); - config = configs[0]; - // Keep track of the entry point where the debug config has been selected, for telemetry purposes. - config.debuggerEvent = DebuggerEvent.debugPanel; - config.configSource = folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile; - } - } - - /** If the config is coming from the "Run and Debug" debugPanel, there are three cases where the folder is undefined: - * 1. when debugging is done on a single file where there is no folder open, - * 2. when the debug configuration is defined at the User level (global). - * 3. when the debug configuration is defined at the workspace level. - * If the config is coming from the "Run and Debug" playButton, there is one case where the folder is undefined: - * 1. when debugging is done on a single file where there is no folder open. - */ - - /** Do not resolve PreLaunchTask for these three cases, and let the Vs Code resolve it: - * 1: The existing configs that are found for a single file. - * 2: The existing configs that come from the playButton (the PreLaunchTask should already be defined for these configs). - * 3: The existing configs that come from the debugPanel where the folder is undefined and the PreLaunchTask cannot be found. - */ - - if (config.preLaunchTask) { - config.configSource = this.getDebugConfigSource(config, folder); - const isIntelliSenseDisabled: boolean = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined).intelliSenseEngine === "disabled"; - // Run the build task if IntelliSense is disabled. - if (isIntelliSenseDisabled) { - try { - await cppBuildTaskProvider.runBuildTask(config.preLaunchTask); - config.preLaunchTask = undefined; - Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "true" }); - } catch (err) { - Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "false" }); - } - return config; - } - let resolveByVsCode: boolean = false; - const isDebugPanel: boolean = !config.debuggerEvent || (config.debuggerEvent && config.debuggerEvent === DebuggerEvent.debugPanel); - const singleFile: boolean = config.configSource === ConfigSource.singleFile; - const isExistingConfig: boolean = this.isExistingConfig(config, folder); - const isExistingTask: boolean = await this.isExistingTask(config, folder); - if (singleFile) { - if (isExistingConfig) { - resolveByVsCode = true; - } - } else { - if (!isDebugPanel && (isExistingConfig || isExistingTask)) { - resolveByVsCode = true; - } else if (isDebugPanel && !folder && isExistingConfig && !isExistingTask) { - resolveByVsCode = true; - } - } - - // Send the telemetry before writing into files - config.debugType = config.debugType ? config.debugType : DebugType.debug; - const configMode: ConfigMode = isExistingConfig ? ConfigMode.launchConfig : ConfigMode.noLaunchConfig; - // if configuration.debuggerEvent === undefined, it means this configuration is already defined in launch.json and is shown in debugPanel. - Telemetry.logDebuggerEvent(config.debuggerEvent || DebuggerEvent.debugPanel, { "debugType": config.debugType || DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": configMode, "cancelled": "false", "succeeded": "true" }); - - if (!resolveByVsCode) { - if (singleFile || (isDebugPanel && !folder && isExistingTask)) { - await this.resolvePreLaunchTask(config, configMode); - config.preLaunchTask = undefined; - } else { - await this.resolvePreLaunchTask(config, configMode, folder); - DebugConfigurationProvider.recentBuildTaskLabelStr = config.preLaunchTask; - } - } else { - DebugConfigurationProvider.recentBuildTaskLabelStr = config.preLaunchTask; - } - } - - // resolveDebugConfigurationWithSubstitutedVariables will be automatically called after this return. - 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. - * If return "undefined", the debugging will be aborted silently. - * If return "null", the debugging will be aborted and launch.json will be opened. - */ - async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, token?: vscode.CancellationToken): Promise { - if (!config || !config.type) { - return undefined; // Abort debugging silently. - } - - 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")); - return undefined; // Abort debugging silently. - } - - // Handle legacy 'externalConsole' bool and convert to console: "externalTerminal" - // eslint-disable-next-line no-prototype-builtins - if (config.hasOwnProperty("externalConsole")) { - void 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; - } - - // Disable debug heap by default, enable if 'enableDebugHeap' is set. - if (!config.enableDebugHeap) { - const disableDebugHeapEnvSetting: Environment = { "name": "_NO_DEBUG_HEAP", "value": "1" }; - - if (config.environment && util.isArray(config.environment)) { - config.environment.push(disableDebugHeapEnvSetting); - } else { - config.environment = [disableDebugHeapEnvSetting]; - } - } - } - - // Add environment variables from .env file - this.resolveEnvFile(config, folder); - - await this.expand(config, folder); - - 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(); - - // 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); - - replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(newPipeProgramStr, debugUtils.ArchType.ia32); - } - - if (replacedPipeProgram) { - config.pipeTransport.pipeProgram = replacedPipeProgram; - } - } - - 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"); - - // 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."); - - 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; - } - } - - if (config.logging?.engineLogging) { - const outputChannel: logger.Logger = logger.getOutputChannelLogger(); - outputChannel.appendLine(localize("debugger.launchConfig", "Launch configuration:")); - outputChannel.appendLine(JSON.stringify(config, undefined, 2)); - // TODO: Enable when https://github.com/microsoft/vscode/issues/108619 is resolved. - // logger.showOutputChannel(); - } - - // Run deploy steps - if (config.deploySteps && config.deploySteps.length !== 0) { - const codeVersion: number[] = util.getVsCodeVersion(); - if ((util.isNumber(codeVersion[0]) && codeVersion[0] < 1) || (util.isNumber(codeVersion[0]) && codeVersion[0] === 1 && util.isNumber(codeVersion[1]) && codeVersion[1] < 69)) { - void logger.getOutputChannelLogger().showErrorMessage(localize("vs.code.1.69+.required", "'deploySteps' require VS Code 1.69+.")); - return undefined; - } - - const deploySucceeded: boolean = await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: localize("running.deploy.steps", "Running deploy steps...") - }, async () => this.deploySteps(config, token)); - - if (!deploySucceeded || token?.isCancellationRequested) { - return undefined; - } - } - - // Pick process if process id is empty - if (config.request === "attach" && !config.processId) { - let processId: string | undefined; - if (config.pipeTransport || config.useExtendedRemote) { - const remoteAttachPicker: RemoteAttachPicker = new RemoteAttachPicker(); - processId = await remoteAttachPicker.ShowAttachEntries(config); - } else { - const attachItemsProvider: AttachItemsProvider = NativeAttachItemsProviderFactory.Get(); - const attacher: AttachPicker = new AttachPicker(attachItemsProvider); - processId = await attacher.ShowAttachEntries(token); - } - - if (processId) { - config.processId = processId; - } else { - void logger.getOutputChannelLogger().showErrorMessage("No process was selected."); - return undefined; - } - } - - return config; - } - - async provideDebugConfigurationsForType(type: DebuggerType, folder?: vscode.WorkspaceFolder, _token?: vscode.CancellationToken): Promise { - const defaultTemplateConfig: CppDebugConfiguration = this.assetProvider.getInitialConfigurations(type).find((config: any) => - isDebugLaunchStr(config.name) && config.request === "launch"); - console.assert(defaultTemplateConfig, "Could not find default debug configuration."); - - const platformInfo: PlatformInformation = await PlatformInformation.GetPlatformInformation(); - - // Import the existing configured tasks from tasks.json file. - const configuredBuildTasks: CppBuildTask[] = await cppBuildTaskProvider.getJsonTasks(); - - let buildTasks: CppBuildTask[] = []; - await this.loadDetectedTasks(); - // Remove the tasks that are already configured once in tasks.json. - const dedupDetectedBuildTasks: CppBuildTask[] = DebugConfigurationProvider.detectedBuildTasks.filter(taskDetected => - !configuredBuildTasks.some(taskJson => taskJson.definition.label === taskDetected.definition.label)); - buildTasks = buildTasks.concat(configuredBuildTasks, dedupDetectedBuildTasks); - - // Filter out build tasks that don't match the currently selected debug configuration type. - if (buildTasks.length !== 0) { - buildTasks = buildTasks.filter((task: CppBuildTask) => { - const command: string = task.definition.command as string; - if (!command) { - return false; - } - if (defaultTemplateConfig.name.startsWith("(Windows) ")) { - if (command.startsWith("cl.exe")) { - return true; - } - } else { - if (!command.startsWith("cl.exe")) { - return true; - } - } - return false; - }); - } - - // Generate new configurations for each build task. - // Generating a task is async, therefore we must *await* *all* map(task => config) Promises to resolve. - let configs: CppDebugConfiguration[] = []; - if (buildTasks.length !== 0) { - configs = (await Promise.all(buildTasks.map>(async task => { - const definition: CppBuildTaskDefinition = task.definition as CppBuildTaskDefinition; - const compilerPath: string = util.isString(definition.command) ? definition.command : definition.command.value; - // Filter out the tasks that has an invalid compiler path. - const compilerPathExists: boolean = path.isAbsolute(compilerPath) ? - // Absolute path, just check if it exists - await util.checkFileExists(compilerPath) : - // Non-absolute. Check on $PATH - (await util.whichAsync(compilerPath) !== undefined); - if (!compilerPathExists) { - logger.getOutputChannelLogger().appendLine(localize('compiler.path.not.exists', "Unable to find {0}. {1} task is ignored.", compilerPath, definition.label)); - } - const compilerName: string = path.basename(compilerPath); - const newConfig: CppDebugConfiguration = { ...defaultTemplateConfig }; // Copy enumerables and properties - newConfig.existing = false; - - newConfig.name = configPrefix + compilerName + " " + this.buildAndDebugActiveFileStr(); - newConfig.preLaunchTask = task.name; - if (newConfig.type === DebuggerType.cppdbg) { - newConfig.externalConsole = false; - } else { - newConfig.console = "integratedTerminal"; - } - // Extract the .exe path from the defined task. - const definedExePath: string | undefined = util.findExePathInArgs(task.definition.args); - newConfig.program = definedExePath ? definedExePath : util.defaultExePath(); - // Add the "detail" property to show the compiler path in QuickPickItem. - // This property will be removed before writing the DebugConfiguration in launch.json. - newConfig.detail = localize("pre.Launch.Task", "preLaunchTask: {0}", task.name); - newConfig.taskDetail = task.detail; - newConfig.taskStatus = task.existing ? - (task.name === DebugConfigurationProvider.recentBuildTaskLabelStr) ? TaskStatus.recentlyUsed : TaskStatus.configured : - TaskStatus.detected; - if (task.isDefault) { - newConfig.isDefault = true; - } - const isCl: boolean = compilerName === "cl.exe"; - newConfig.cwd = isWindows && !isCl && !process.env.PATH?.includes(path.dirname(compilerPath)) ? path.dirname(compilerPath) : "${fileDirname}"; - - if (platformInfo.platform !== "darwin") { - let debuggerName: string; - if (compilerName.startsWith("clang")) { - newConfig.MIMode = "lldb"; - if (isWindows) { - debuggerName = "lldb"; - } else { - debuggerName = "lldb-mi"; - // Search for clang-8, clang-10, etc. - if ((compilerName !== "clang-cl.exe") && (compilerName !== "clang-cpp.exe")) { - const suffixIndex: number = compilerName.indexOf("-"); - if (suffixIndex !== -1) { - const suffix: string = compilerName.substring(suffixIndex); - debuggerName += suffix; - } - } - } - newConfig.type = DebuggerType.cppdbg; - } else if (compilerName === "cl.exe") { - newConfig.miDebuggerPath = undefined; - newConfig.type = DebuggerType.cppvsdbg; - return newConfig; - } else { - debuggerName = "gdb"; - } - if (isWindows) { - debuggerName = debuggerName.endsWith(".exe") ? debuggerName : (debuggerName + ".exe"); - } - 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; - } 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; - } - } - } - return newConfig; - }))).filter((item): item is CppDebugConfiguration => !!item); - } - configs.push(defaultTemplateConfig); - const existingConfigs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, type)?.map(config => { - if (!config.detail && config.preLaunchTask) { - config.detail = localize("pre.Launch.Task", "preLaunchTask: {0}", config.preLaunchTask); - } - config.existing = true; - return config; - }); - if (existingConfigs) { - // Remove the detected configs that are already configured once in launch.json. - const dedupExistingConfigs: CppDebugConfiguration[] = configs.filter(detectedConfig => !existingConfigs.some(config => { - if (config.preLaunchTask === detectedConfig.preLaunchTask && config.type === detectedConfig.type && config.request === detectedConfig.request) { - // Carry the default task information. - config.isDefault = detectedConfig.isDefault ? detectedConfig.isDefault : undefined; - return true; - } - return false; - })); - configs = existingConfigs.concat(dedupExistingConfigs); - } - return configs; - } - - private async loadDetectedTasks(): Promise { - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - const emptyTasks: CppBuildTask[] = []; - if (!editor) { - DebugConfigurationProvider.detectedBuildTasks = emptyTasks; - return; - } - - const fileExt: string = path.extname(editor.document.fileName); - if (!fileExt) { - DebugConfigurationProvider.detectedBuildTasks = emptyTasks; - return; - } - - // Don't offer tasks for header files. - const isHeader: boolean = util.isHeaderFile(editor.document.uri); - if (isHeader) { - DebugConfigurationProvider.detectedBuildTasks = emptyTasks; - return; - } - - // Don't offer tasks if the active file's extension is not a recognized C/C++ extension. - const fileIsCpp: boolean = util.isCppFile(editor.document.uri); - const fileIsC: boolean = util.isCFile(editor.document.uri); - if (!(fileIsCpp || fileIsC)) { - DebugConfigurationProvider.detectedBuildTasks = emptyTasks; - return; - } - - if (fileIsCpp) { - if (!DebugConfigurationProvider.detectedCppBuildTasks || DebugConfigurationProvider.detectedCppBuildTasks.length === 0) { - DebugConfigurationProvider.detectedCppBuildTasks = await cppBuildTaskProvider.getTasks(true); - } - DebugConfigurationProvider.detectedBuildTasks = DebugConfigurationProvider.detectedCppBuildTasks; - } else { - if (!DebugConfigurationProvider.detectedCBuildTasks || DebugConfigurationProvider.detectedCBuildTasks.length === 0) { - DebugConfigurationProvider.detectedCBuildTasks = await cppBuildTaskProvider.getTasks(true); - } - DebugConfigurationProvider.detectedBuildTasks = DebugConfigurationProvider.detectedCBuildTasks; - } - } - - public static get recentBuildTaskLabelStr(): string { - return DebugConfigurationProvider.recentBuildTaskLabel; - } - - public static set recentBuildTaskLabelStr(recentTask: string) { - DebugConfigurationProvider.recentBuildTaskLabel = recentTask; - } - - private buildAndDebugActiveFileStr(): string { - return `${localize("build.and.debug.active.file", 'build and debug active file')}`; - } - - private isClConfiguration(configurationLabel: string): boolean { - return configurationLabel.startsWith("C/C++: cl.exe"); - } - - private showErrorIfClNotAvailable(_configurationLabel: string): boolean { - if (!process.env.DevEnvDir || process.env.DevEnvDir.length === 0) { - void 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 true; - } - return false; - } - - private getLLDBFrameworkPath(): string | undefined { - const LLDBFramework: string = "LLDB.framework"; - // Note: When adding more search paths, make sure the shipped lldb-mi also has it. See Build/lldb-mi.yml and 'install_name_tool' commands. - const searchPaths: string[] = [ - "/Library/Developer/CommandLineTools/Library/PrivateFrameworks", // XCode CLI - "/Applications/Xcode.app/Contents/SharedFrameworks" // App Store XCode - ]; - - for (const searchPath of searchPaths) { - if (fs.existsSync(path.join(searchPath, LLDBFramework))) { - // Found a framework that 'lldb-mi' can use. - return searchPath; - } - } - - const outputChannel: logger.Logger = 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: string = "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: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): void { - if (config.envFile) { - // replace ${env:???} variables - let envFilePath: string = util.resolveVariables(config.envFile, undefined); - - try { - if (folder && folder.uri && folder.uri.fsPath) { - // Try to replace ${workspaceFolder} or ${workspaceRoot} - envFilePath = envFilePath.replace(/(\${workspaceFolder}|\${workspaceRoot})/g, folder.uri.fsPath); - } - - const parsedFile: ParsedEnvironmentFile = ParsedEnvironmentFile.CreateFromFile(envFilePath, config["environment"]); - - // show error message if single lines cannot get parsed - if (parsedFile.Warning) { - void DebugConfigurationProvider.showFileWarningAsync(parsedFile.Warning, config.envFile); - } - - config.environment = parsedFile.Env; - - delete config.envFile; - } catch (errJS) { - const e: Error = errJS as Error; - throw new Error(localize("envfile.failed", "Failed to use {0}. Reason: {1}", "envFile", e.message)); - } - } - } - - private resolveSourceFileMapVariables(config: CppDebugConfiguration): void { - const messages: string[] = []; - if (config.sourceFileMap) { - for (const sourceFileMapSource of Object.keys(config.sourceFileMap)) { - let message: string = ""; - const sourceFileMapTarget: string = config.sourceFileMap[sourceFileMapSource]; - - let source: string = sourceFileMapSource; - let target: string | object = sourceFileMapTarget; - - // TODO: pass config.environment as 'additionalEnvironment' to resolveVariables when it is { key: value } instead of { "key": key, "value": value } - const newSourceFileMapSource: string = util.resolveVariables(sourceFileMapSource, undefined); - if (sourceFileMapSource !== newSourceFileMapSource) { - message = "\t" + localize("replacing.sourcepath", "Replacing {0} '{1}' with '{2}'.", "sourcePath", sourceFileMapSource, newSourceFileMapSource); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete config.sourceFileMap[sourceFileMapSource]; - source = newSourceFileMapSource; - } - - if (util.isString(sourceFileMapTarget)) { - const newSourceFileMapTarget: string = util.resolveVariables(sourceFileMapTarget, undefined); - if (sourceFileMapTarget !== newSourceFileMapTarget) { - // Add a space if source was changed, else just tab the target message. - message += message ? ' ' : '\t'; - message += localize("replacing.targetpath", "Replacing {0} '{1}' with '{2}'.", "targetPath", sourceFileMapTarget, newSourceFileMapTarget); - target = newSourceFileMapTarget; - } - } else if (util.isObject(sourceFileMapTarget)) { - const newSourceFileMapTarget: { "editorPath": string; "useForBreakpoints": boolean } = sourceFileMapTarget; - newSourceFileMapTarget["editorPath"] = util.resolveVariables(sourceFileMapTarget["editorPath"], undefined); - - if (sourceFileMapTarget !== newSourceFileMapTarget) { - // Add a space if source was changed, else just tab the target message. - 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: string, fileName: string): Promise { - const openItem: vscode.MessageItem = { title: localize("open.envfile", "Open {0}", "envFile") }; - const result: vscode.MessageItem | undefined = await vscode.window.showWarningMessage(message, openItem); - if (result && result.title === openItem.title) { - const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(fileName); - if (doc) { - void vscode.window.showTextDocument(doc); - } - } - } - - private localizeConfigDetail(items: ConfigMenu[]): ConfigMenu[] { - items.map((item: ConfigMenu) => { - switch (item.detail) { - case TaskStatus.recentlyUsed: { - item.detail = localize("recently.used.task", "Recently Used Task"); - break; - } - case TaskStatus.configured: { - item.detail = localize("configured.task", "Configured Task"); - break; - } - case TaskStatus.detected: { - item.detail = localize("detected.task", "Detected Task"); - break; - } - default: { - break; - } - } - if (item.configuration.taskDetail) { - // Add the compiler path of the preLaunchTask to the description of the debug configuration. - item.detail = (item.detail ?? "") + " (" + item.configuration.taskDetail + ")"; - } - }); - return items; - } - - private findDefaultConfig(configs: CppDebugConfiguration[]): CppDebugConfiguration[] { - // eslint-disable-next-line no-prototype-builtins - return configs.filter((config: CppDebugConfiguration) => config.hasOwnProperty("isDefault") && config.isDefault); - } - - private async isExistingTask(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): Promise { - if (config.taskStatus && (config.taskStatus !== TaskStatus.detected)) { - return true; - } else if (config.taskStatus && (config.taskStatus === TaskStatus.detected)) { - return false; - } - return cppBuildTaskProvider.isExistingTask(config.preLaunchTask, folder); - } - - private isExistingConfig(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): boolean { - if (config.existing) { - return config.existing; - } - const configs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, config.type); - if (configs && configs.length > 0) { - const selectedConfig: any | undefined = configs.find((item: any) => item.name && item.name === config.name); - if (selectedConfig) { - return true; - } - } - return false; - } - - private getDebugConfigSource(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): ConfigSource | undefined { - if (config.configSource) { - return config.configSource; - } - const isExistingConfig: boolean = this.isExistingConfig(config, folder); - if (!isExistingConfig && !folder) { - return ConfigSource.singleFile; - } else if (!isExistingConfig) { - return ConfigSource.workspaceFolder; - } - - const configs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, config.type); - const matchingConfig: CppDebugConfiguration | undefined = configs?.find((item: any) => item.name && item.name === config.name); - if (matchingConfig?.configSource) { - return matchingConfig.configSource; - } - return ConfigSource.unknown; - } - - public getLaunchConfigs(folder?: vscode.WorkspaceFolder, type?: DebuggerType | string): CppDebugConfiguration[] | undefined { - // Get existing debug configurations from launch.json or user/workspace "launch" settings. - const WorkspaceConfigs: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('launch', folder); - const configs: any = WorkspaceConfigs.inspect('configurations'); - if (!configs) { - return undefined; - } - let detailedConfigs: CppDebugConfiguration[] = []; - if (configs.workspaceFolderValue !== undefined) { - detailedConfigs = detailedConfigs.concat(configs.workspaceFolderValue.map((item: CppDebugConfiguration) => { - item.configSource = ConfigSource.workspaceFolder; - return item; - })); - } - if (configs.workspaceValue !== undefined) { - detailedConfigs = detailedConfigs.concat(configs.workspaceValue.map((item: CppDebugConfiguration) => { - item.configSource = ConfigSource.workspace; - return item; - })); - } - if (configs.globalValue !== undefined) { - detailedConfigs = detailedConfigs.concat(configs.globalValue.map((item: CppDebugConfiguration) => { - item.configSource = ConfigSource.global; - return item; - })); - } - detailedConfigs = detailedConfigs.filter((config: any) => config.name && config.request === "launch" && type ? (config.type === type) : true); - return detailedConfigs; - } - - private getLaunchJsonPath(): string | undefined { - return util.getJsonPath("launch.json"); - } - - private getRawLaunchJson(): Promise { - const path: string | undefined = this.getLaunchJsonPath(); - return util.getRawJson(path); - } - - public async writeDebugConfig(config: vscode.DebugConfiguration, isExistingConfig: boolean, _folder?: vscode.WorkspaceFolder): Promise { - const launchJsonPath: string | undefined = this.getLaunchJsonPath(); - - if (isExistingConfig) { - if (launchJsonPath) { - const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(launchJsonPath); - if (doc) { - void vscode.window.showTextDocument(doc); - } - } - return; - } - const rawLaunchJson: any = await this.getRawLaunchJson(); - if (!rawLaunchJson.configurations) { - rawLaunchJson.configurations = []; - } - if (!rawLaunchJson.version) { - rawLaunchJson.version = "2.0.0"; - } - - // Remove the extra properties that are not a part of the vsCode.DebugConfiguration. - config.detail = undefined; - config.taskStatus = undefined; - config.isDefault = undefined; - config.source = undefined; - config.debuggerEvent = undefined; - config.debugType = undefined; - config.existing = undefined; - config.taskDetail = undefined; - rawLaunchJson.configurations.push(config); - - if (!launchJsonPath) { - throw new Error("Failed to get tasksJsonPath in checkBuildTaskExists()"); - } - - const settings: OtherSettings = new OtherSettings(); - await util.writeFileText(launchJsonPath, jsonc.stringify(rawLaunchJson, null, settings.editorTabSize)); - await vscode.workspace.openTextDocument(launchJsonPath); - const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(launchJsonPath); - if (doc) { - void vscode.window.showTextDocument(doc); - } - } - - public async addDebugConfiguration(textEditor: vscode.TextEditor): Promise { - const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); - if (!folder) { - return; - } - const selectedConfig: vscode.DebugConfiguration | undefined = await this.selectConfiguration(textEditor, false, true); - if (!selectedConfig) { - Telemetry.logDebuggerEvent(DebuggerEvent.addConfigGear, { "configSource": ConfigSource.workspaceFolder, "configMode": ConfigMode.launchConfig, "cancelled": "true", "succeeded": "true" }); - return; // User canceled it. - } - - const isExistingConfig: boolean = this.isExistingConfig(selectedConfig, folder); - // Write preLaunchTask into tasks.json file. - if (!isExistingConfig && selectedConfig.preLaunchTask && (selectedConfig.taskStatus && selectedConfig.taskStatus === TaskStatus.detected)) { - 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; - // 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" }); - } - - public async buildAndRun(textEditor: vscode.TextEditor): Promise { - // Turn off the debug mode. - return this.buildAndDebug(textEditor, false); - } - - public async buildAndDebug(textEditor: vscode.TextEditor, debugModeOn: boolean = true): Promise { - let folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); - const selectedConfig: CppDebugConfiguration | undefined = await this.selectConfiguration(textEditor); - if (!selectedConfig) { - Telemetry.logDebuggerEvent(DebuggerEvent.playButton, { "debugType": debugModeOn ? DebugType.debug : DebugType.run, "configSource": ConfigSource.unknown, "cancelled": "true", "succeeded": "true" }); - return; // User canceled it. - } - - // Keep track of the entry point where the debug has been selected, for telemetry purposes. - selectedConfig.debuggerEvent = DebuggerEvent.playButton; - // If the configs are coming from workspace or global settings and the task is not found in tasks.json, let that to be resolved by VsCode. - if (selectedConfig.preLaunchTask && selectedConfig.configSource && - (selectedConfig.configSource === ConfigSource.global || selectedConfig.configSource === ConfigSource.workspace) && - !await this.isExistingTask(selectedConfig)) { - folder = undefined; - } - selectedConfig.debugType = debugModeOn ? DebugType.debug : DebugType.run; - // startDebugging will trigger a call to resolveDebugConfiguration. - await vscode.debug.startDebugging(folder, selectedConfig, { noDebug: !debugModeOn }); - } - - private async selectConfiguration(textEditor: vscode.TextEditor, pickDefault: boolean = true, onlyWorkspaceFolder: boolean = false): Promise { - const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); - if (!util.isCppOrCFile(textEditor.document.uri)) { - void vscode.window.showErrorMessage(localize("cannot.build.non.cpp", 'Cannot build and debug because the active file is not a C or C++ source file.')); - return; - } - - // 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)); - } - if (onlyWorkspaceFolder) { - configs = configs.filter(item => !item.configSource || item.configSource === ConfigSource.workspaceFolder); - } - - const defaultConfig: CppDebugConfiguration[] | undefined = pickDefault ? this.findDefaultConfig(configs) : undefined; - - const items: ConfigMenu[] = configs.map(config => ({ label: config.name, configuration: config, description: config.detail, detail: config.taskStatus })); - - let selection: ConfigMenu | undefined; - - // if there was only one config for the default task, choose that config, otherwise ask the user to choose. - if (defaultConfig && defaultConfig.length === 1) { - selection = { label: defaultConfig[0].name, configuration: defaultConfig[0], description: defaultConfig[0].detail, detail: defaultConfig[0].taskStatus }; - } else { - let sortedItems: ConfigMenu[] = []; - // Find the recently used task and place it at the top of quickpick list. - const recentTask: ConfigMenu[] = items.filter(item => item.configuration.preLaunchTask && item.configuration.preLaunchTask === DebugConfigurationProvider.recentBuildTaskLabelStr); - if (recentTask.length !== 0 && recentTask[0].detail !== TaskStatus.detected) { - recentTask[0].detail = TaskStatus.recentlyUsed; - sortedItems.push(recentTask[0]); - } - sortedItems = sortedItems.concat(items.filter(item => item.detail === TaskStatus.configured)); - sortedItems = sortedItems.concat(items.filter(item => item.detail === TaskStatus.detected)); - sortedItems = sortedItems.concat(items.filter(item => item.detail === undefined)); - - selection = await vscode.window.showQuickPick(this.localizeConfigDetail(sortedItems), { - placeHolder: items.length === 0 ? localize("no.compiler.found", "No compiler found") : localize("select.debug.configuration", "Select a debug configuration") - }); - } - if (selection && this.isClConfiguration(selection.configuration.name) && this.showErrorIfClNotAvailable(selection.configuration.name)) { - return; - } - return selection?.configuration; - } - - private async resolvePreLaunchTask(config: CppDebugConfiguration, configMode: ConfigMode, folder?: vscode.WorkspaceFolder | undefined): Promise { - if (config.preLaunchTask) { - try { - if (config.configSource === ConfigSource.singleFile) { - // In case of singleFile, remove the preLaunch task from the debug configuration and run it here instead. - await cppBuildTaskProvider.runBuildTask(config.preLaunchTask); - } else { - await cppBuildTaskProvider.writeDefaultBuildTask(config.preLaunchTask, folder); - } - } catch (errJS) { - const e: Error = errJS as Error; - if (e && e.message === util.failedToParseJson) { - void vscode.window.showErrorMessage(util.failedToParseJson); - } - Telemetry.logDebuggerEvent(config.debuggerEvent || DebuggerEvent.debugPanel, { "debugType": config.debugType || DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": configMode, "cancelled": "false", "succeeded": "false" }); - } - } - } - - private async expand(config: vscode.DebugConfiguration, folder: vscode.WorkspaceFolder | undefined): Promise { - const folderPath: string | undefined = folder?.uri.fsPath || vscode.workspace.workspaceFolders?.[0].uri.fsPath; - const vars: ExpansionVars = config.variables ? config.variables : {}; - vars.workspaceFolder = folderPath || '{workspaceFolder}'; - vars.workspaceFolderBasename = folderPath ? path.basename(folderPath) : '{workspaceFolderBasename}'; - const expansionOptions: ExpansionOptions = { vars, recursive: true }; - return expandAllStrings(config, expansionOptions); - } - - // Returns true when ALL steps succeed; stop all subsequent steps if one fails - private async deploySteps(config: vscode.DebugConfiguration, cancellationToken?: vscode.CancellationToken): Promise { - let succeeded: boolean = true; - const deployStart: number = new Date().getTime(); - - for (const step of config.deploySteps) { - succeeded = await this.singleDeployStep(config, step, cancellationToken); - if (!succeeded) { - break; - } - } - - const deployEnd: number = new Date().getTime(); - - const telemetryProperties: { [key: string]: string } = { - Succeeded: `${succeeded}`, - IsDebugging: `${!config.noDebug || false}` - }; - const telemetryMetrics: { [key: string]: number } = { - NumSteps: config.deploySteps.length, - Duration: deployEnd - deployStart - }; - Telemetry.logDebuggerEvent('deploy', telemetryProperties, telemetryMetrics); - - return succeeded; - } - - private async singleDeployStep(config: vscode.DebugConfiguration, step: any, cancellationToken?: vscode.CancellationToken): Promise { - if ((config.noDebug && step.debug === true) || (!config.noDebug && step.debug === false)) { - // Skip steps that doesn't match current launch mode. Explicit true/false check, since a step is always run when debug is undefined. - return true; - } - const stepType: StepType = step.type; - switch (stepType) { - case StepType.command: { - // VS Code commands are the same regardless of which extension invokes them, so just invoke them here. - if (step.args && !Array.isArray(step.args)) { - void logger.getOutputChannelLogger().showErrorMessage(localize('command.args.must.be.array', '"args" in command deploy step must be an array.')); - return false; - } - const returnCode: unknown = await vscode.commands.executeCommand(step.command, ...step.args); - return !returnCode; - } - case StepType.scp: - case StepType.rsync: { - const isScp: boolean = stepType === StepType.scp; - if (!step.files || !step.targetDir || !step.host) { - void logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.copyFile', '"host", "files", and "targetDir" are required in {0} steps.', isScp ? 'SCP' : 'rsync')); - return false; - } - const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port }; - const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts; - let files: vscode.Uri[] = []; - if (util.isString(step.files)) { - files = files.concat((await globAsync(step.files)).map(file => vscode.Uri.file(file))); - } else if (util.isArrayOfString(step.files)) { - for (const fileGlob of (step.files as string[])) { - files = files.concat((await globAsync(fileGlob)).map(file => vscode.Uri.file(file))); - } - } else { - void logger.getOutputChannelLogger().showErrorMessage(localize('incorrect.files.type.copyFile', '"files" must be a string or an array of strings in {0} steps.', isScp ? 'SCP' : 'rsync')); - return false; - } - - let scpResult: util.ProcessReturnType; - if (isScp) { - scpResult = await scp(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); - } else { - scpResult = await rsync(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); - } - - if (!scpResult.succeeded || cancellationToken?.isCancellationRequested) { - return false; - } - break; - } - case StepType.ssh: { - if (!step.host || !step.command) { - void logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.ssh', '"host" and "command" are required for ssh steps.')); - return false; - } - const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port }; - 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); - if (!sshResult.succeeded || cancellationToken?.isCancellationRequested) { - return false; - } - break; - } - case StepType.shell: { - if (!step.command) { - 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); - if (!taskResult.succeeded || cancellationToken?.isCancellationRequested) { - void logger.getOutputChannelLogger().showErrorMessage(taskResult.output); - return false; - } - break; - } - default: { - logger.getOutputChannelLogger().appendLine(localize('deploy.step.type.not.supported', 'Deploy step type {0} is not supported.', step.type)); - return false; - } - } - return true; - } -} - -export interface IConfigurationAssetProvider { - getInitialConfigurations(debuggerType: DebuggerType): any; - getConfigurationSnippets(): vscode.CompletionItem[]; -} - -export class ConfigurationAssetProviderFactory { - public static getConfigurationProvider(): IConfigurationAssetProvider { - 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: 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 { - 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 convertConfigurationSnippetToCompletionItem(snippet: IConfigurationSnippet): vscode.CompletionItem { - const item: vscode.CompletionItem = new vscode.CompletionItem(snippet.label, vscode.CompletionItemKind.Module); - - item.insertText = snippet.bodyText; - - return item; -} - -export class ConfigurationSnippetProvider implements vscode.CompletionItemProvider { - private provider: IConfigurationAssetProvider; - private snippets: vscode.CompletionItem[]; - - constructor(provider: IConfigurationAssetProvider) { - this.provider = provider; - this.snippets = this.provider.getConfigurationSnippets(); - } - public resolveCompletionItem(item: vscode.CompletionItem, _token: vscode.CancellationToken): Thenable { - return Promise.resolve(item); - } - - // This function will only provide completion items via the Add Configuration Button - // There are two cases where the configuration array has nothing or has some items. - // 1. If it has nothing, insert a snippet the user selected. - // 2. If there are items, the Add Configuration button will append it to the start of the configuration array. This function inserts a comma at the end of the snippet. - public provideCompletionItems(document: vscode.TextDocument, _position: vscode.Position, _token: vscode.CancellationToken, _context: vscode.CompletionContext): Thenable { - let items: vscode.CompletionItem[] = this.snippets; - let hasLaunchConfigs: boolean = false; - try { - const launch: any = jsonc.parse(document.getText()); - hasLaunchConfigs = launch.configurations.length !== 0; - } catch { - // ignore - } - - // Check to see if the array is empty, so any additional inserted snippets will need commas. - if (hasLaunchConfigs) { - items = []; - - // Make a copy of each snippet since we are adding a comma to the end of the insertText. - this.snippets.forEach((item) => items.push({ ...item })); - - items.map((item) => { - item.insertText = item.insertText + ','; // Add comma - }); - } - - return Promise.resolve(new vscode.CompletionList(items, true)); - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as jsonc from 'comment-json'; +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as os from 'os'; +import * as path from 'path'; +import { promisify } from 'util'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import * as util from '../common'; +import { isWindows } from '../constants'; +import { expandAllStrings, ExpansionOptions, ExpansionVars } from '../expand'; +import { CppBuildTask, CppBuildTaskDefinition, cppBuildTaskProvider } from '../LanguageServer/cppBuildTaskProvider'; +import { configPrefix } from '../LanguageServer/extension'; +import { CppSettings, OtherSettings } from '../LanguageServer/settings'; +import { getOutputChannel, getOutputChannelLogger, Logger, showOutputChannel } 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 { NativeAttachItemsProviderFactory } from './nativeAttach'; +import { Environment, ParsedEnvironmentFile } from './ParsedEnvironmentFile'; +import * as debugUtils from './utils'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +enum StepType { + scp = 'scp', + rsync = 'rsync', + ssh = 'ssh', + shell = 'shell', + remoteShell = 'remoteShell', + command = 'command' +} + +const globAsync: (pattern: string, options?: glob.IOptions | undefined) => Promise = promisify(glob); + +/* + * Retrieves configurations from a provider and displays them in a quickpick menu to be selected. + * Ensures that the selected configuration's preLaunchTask (if existent) is populated in the user's task.json. + * Automatically starts debugging for "Build and Debug" configurations. + */ +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 static ClearDetectedBuildTasks(): void { + DebugConfigurationProvider.detectedCppBuildTasks = []; + DebugConfigurationProvider.detectedCBuildTasks = []; + } + + /** + * Returns a list of initial debug configurations based on contextual information, e.g. package.json or folder. + * resolveDebugConfiguration will be automatically called after this function. + */ + async provideDebugConfigurations(folder?: vscode.WorkspaceFolder, token?: vscode.CancellationToken): Promise { + let configs: CppDebugConfiguration[] | null | undefined = await this.provideDebugConfigurationsForType(this.type, folder, token); + if (!configs) { + configs = []; + } + const defaultTemplateConfig: CppDebugConfiguration | undefined = configs.find(config => isDebugLaunchStr(config.name) && config.request === "launch"); + if (!defaultTemplateConfig) { + throw new Error("Default config not found in provideDebugConfigurations()"); + } + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!editor || !util.isCppOrCFile(editor.document.uri) || configs.length <= 1) { + return [defaultTemplateConfig]; + } + + const defaultConfig: CppDebugConfiguration[] = this.findDefaultConfig(configs); + // If there was only one config defined for the default task, choose that config, otherwise ask the user to choose. + if (defaultConfig.length === 1) { + return defaultConfig; + } + + // Find the recently used task and place it at the top of quickpick list. + let recentlyUsedConfig: CppDebugConfiguration | undefined; + configs = configs.filter(config => { + if (config.taskStatus !== TaskStatus.recentlyUsed) { + return true; + } else { + recentlyUsedConfig = config; + return false; + } + }); + if (recentlyUsedConfig) { + configs.unshift(recentlyUsedConfig); + } + + const items: ConfigMenu[] = configs.map(config => { + const quickPickConfig: CppDebugConfiguration = { ...config }; + const menuItem: ConfigMenu = { label: config.name, configuration: quickPickConfig, description: config.detail, detail: config.taskStatus }; + // Rename the menu item for the default configuration as its name is non-descriptive. + if (isDebugLaunchStr(menuItem.label)) { + menuItem.label = localize("default.configuration.menuitem", "Default Configuration"); + } + return menuItem; + }); + + const selection: ConfigMenu | undefined = await vscode.window.showQuickPick(this.localizeConfigDetail(items), { placeHolder: localize("select.configuration", "Select a configuration") }); + if (!selection) { + Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": "debug", "configSource": ConfigSource.unknown, "configMode": ConfigMode.unknown, "cancelled": "true", "succeeded": "true" }); + return []; // User canceled it. + } + + if (this.isClConfiguration(selection.label)) { + this.showErrorIfClNotAvailable(selection.label); + } + + return [selection.configuration]; + } + + /** + * Error checks the provided 'config' without any variables substituted. + * If return "undefined", the debugging will be aborted silently. + * If return "null", the debugging will be aborted and launch.json will be opened. + * resolveDebugConfigurationWithSubstitutedVariables will be automatically called after this function. + */ + async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, _token?: vscode.CancellationToken): Promise { + if (!config || !config.type) { + // When DebugConfigurationProviderTriggerKind is Dynamic, this function will be called with an empty config. + // Hence, providing debug configs, and start debugging should be done manually. + // resolveDebugConfiguration will be automatically called after calling provideDebugConfigurations. + const configs: CppDebugConfiguration[] = await this.provideDebugConfigurations(folder); + if (!configs || configs.length === 0) { + Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile, "configMode": ConfigMode.noLaunchConfig, "cancelled": "true", "succeeded": "true" }); + return undefined; // aborts debugging silently + } else { + // Currently, we expect only one debug config to be selected. + console.assert(configs.length === 1, "More than one debug config is selected."); + config = configs[0]; + // Keep track of the entry point where the debug config has been selected, for telemetry purposes. + config.debuggerEvent = DebuggerEvent.debugPanel; + config.configSource = folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile; + } + } + + /** If the config is coming from the "Run and Debug" debugPanel, there are three cases where the folder is undefined: + * 1. when debugging is done on a single file where there is no folder open, + * 2. when the debug configuration is defined at the User level (global). + * 3. when the debug configuration is defined at the workspace level. + * If the config is coming from the "Run and Debug" playButton, there is one case where the folder is undefined: + * 1. when debugging is done on a single file where there is no folder open. + */ + + /** Do not resolve PreLaunchTask for these three cases, and let the Vs Code resolve it: + * 1: The existing configs that are found for a single file. + * 2: The existing configs that come from the playButton (the PreLaunchTask should already be defined for these configs). + * 3: The existing configs that come from the debugPanel where the folder is undefined and the PreLaunchTask cannot be found. + */ + + if (config.preLaunchTask) { + config.configSource = this.getDebugConfigSource(config, folder); + const isIntelliSenseDisabled: boolean = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined).intelliSenseEngine === "disabled"; + // Run the build task if IntelliSense is disabled. + if (isIntelliSenseDisabled) { + try { + await cppBuildTaskProvider.runBuildTask(config.preLaunchTask); + config.preLaunchTask = undefined; + Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "true" }); + } catch (err) { + Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "false" }); + } + return config; + } + let resolveByVsCode: boolean = false; + const isDebugPanel: boolean = !config.debuggerEvent || (config.debuggerEvent && config.debuggerEvent === DebuggerEvent.debugPanel); + const singleFile: boolean = config.configSource === ConfigSource.singleFile; + const isExistingConfig: boolean = this.isExistingConfig(config, folder); + const isExistingTask: boolean = await this.isExistingTask(config, folder); + if (singleFile) { + if (isExistingConfig) { + resolveByVsCode = true; + } + } else { + if (!isDebugPanel && (isExistingConfig || isExistingTask)) { + resolveByVsCode = true; + } else if (isDebugPanel && !folder && isExistingConfig && !isExistingTask) { + resolveByVsCode = true; + } + } + + // Send the telemetry before writing into files + config.debugType = config.debugType ? config.debugType : DebugType.debug; + const configMode: ConfigMode = isExistingConfig ? ConfigMode.launchConfig : ConfigMode.noLaunchConfig; + // if configuration.debuggerEvent === undefined, it means this configuration is already defined in launch.json and is shown in debugPanel. + Telemetry.logDebuggerEvent(config.debuggerEvent || DebuggerEvent.debugPanel, { "debugType": config.debugType || DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": configMode, "cancelled": "false", "succeeded": "true" }); + + if (!resolveByVsCode) { + if (singleFile || (isDebugPanel && !folder && isExistingTask)) { + await this.resolvePreLaunchTask(config, configMode); + config.preLaunchTask = undefined; + } else { + await this.resolvePreLaunchTask(config, configMode, folder); + DebugConfigurationProvider.recentBuildTaskLabelStr = config.preLaunchTask; + } + } else { + DebugConfigurationProvider.recentBuildTaskLabelStr = config.preLaunchTask; + } + } + + // resolveDebugConfigurationWithSubstitutedVariables will be automatically called after this return. + 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. + * If return "undefined", the debugging will be aborted silently. + * If return "null", the debugging will be aborted and launch.json will be opened. + */ + async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, token?: vscode.CancellationToken): Promise { + if (!config || !config.type) { + return undefined; // Abort debugging silently. + } + + if (config.type === DebuggerType.cppvsdbg) { + // Fail if cppvsdbg type is running on non-Windows + if (os.platform() !== 'win32') { + void 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; // Abort debugging silently. + } + + // Handle legacy 'externalConsole' bool and convert to console: "externalTerminal" + // eslint-disable-next-line no-prototype-builtins + if (config.hasOwnProperty("externalConsole")) { + void 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; + } + + // Disable debug heap by default, enable if 'enableDebugHeap' is set. + if (!config.enableDebugHeap) { + const disableDebugHeapEnvSetting: Environment = { "name": "_NO_DEBUG_HEAP", "value": "1" }; + + if (config.environment && util.isArray(config.environment)) { + config.environment.push(disableDebugHeapEnvSetting); + } else { + config.environment = [disableDebugHeapEnvSetting]; + } + } + } + + // Add environment variables from .env file + this.resolveEnvFile(config, folder); + + await this.expand(config, folder); + + 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(); + + // 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); + + replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(newPipeProgramStr, debugUtils.ArchType.ia32); + } + + if (replacedPipeProgram) { + config.pipeTransport.pipeProgram = replacedPipeProgram; + } + } + + 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"); + + // 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."); + + 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; + } + } + + if (config.logging?.engineLogging) { + const outputChannel: Logger = getOutputChannelLogger(); + outputChannel.appendLine(localize("debugger.launchConfig", "Launch configuration:")); + outputChannel.appendLine(JSON.stringify(config, undefined, 2)); + // TODO: Enable when https://github.com/microsoft/vscode/issues/108619 is resolved. + // showOutputChannel(); + } + + // Run deploy steps + if (config.deploySteps && config.deploySteps.length !== 0) { + const codeVersion: number[] = util.getVsCodeVersion(); + if ((util.isNumber(codeVersion[0]) && codeVersion[0] < 1) || (util.isNumber(codeVersion[0]) && codeVersion[0] === 1 && util.isNumber(codeVersion[1]) && codeVersion[1] < 69)) { + void getOutputChannelLogger().showErrorMessage(localize("vs.code.1.69+.required", "'deploySteps' require VS Code 1.69+.")); + return undefined; + } + + const deploySucceeded: boolean = await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: localize("running.deploy.steps", "Running deploy steps...") + }, async () => this.deploySteps(config, token)); + + if (!deploySucceeded || token?.isCancellationRequested) { + return undefined; + } + } + + // Pick process if process id is empty + if (config.request === "attach" && !config.processId) { + let processId: string | undefined; + if (config.pipeTransport || config.useExtendedRemote) { + const remoteAttachPicker: RemoteAttachPicker = new RemoteAttachPicker(); + processId = await remoteAttachPicker.ShowAttachEntries(config); + } else { + const attachItemsProvider: AttachItemsProvider = NativeAttachItemsProviderFactory.Get(); + const attacher: AttachPicker = new AttachPicker(attachItemsProvider); + processId = await attacher.ShowAttachEntries(token); + } + + if (processId) { + config.processId = processId; + } else { + void getOutputChannelLogger().showErrorMessage("No process was selected."); + return undefined; + } + } + + return config; + } + + async provideDebugConfigurationsForType(type: DebuggerType, folder?: vscode.WorkspaceFolder, _token?: vscode.CancellationToken): Promise { + const defaultTemplateConfig: CppDebugConfiguration = this.assetProvider.getInitialConfigurations(type).find((config: any) => + isDebugLaunchStr(config.name) && config.request === "launch"); + console.assert(defaultTemplateConfig, "Could not find default debug configuration."); + + const platformInfo: PlatformInformation = await PlatformInformation.GetPlatformInformation(); + + // Import the existing configured tasks from tasks.json file. + const configuredBuildTasks: CppBuildTask[] = await cppBuildTaskProvider.getJsonTasks(); + + let buildTasks: CppBuildTask[] = []; + await this.loadDetectedTasks(); + // Remove the tasks that are already configured once in tasks.json. + const dedupDetectedBuildTasks: CppBuildTask[] = DebugConfigurationProvider.detectedBuildTasks.filter(taskDetected => + !configuredBuildTasks.some(taskJson => taskJson.definition.label === taskDetected.definition.label)); + buildTasks = buildTasks.concat(configuredBuildTasks, dedupDetectedBuildTasks); + + // Filter out build tasks that don't match the currently selected debug configuration type. + if (buildTasks.length !== 0) { + buildTasks = buildTasks.filter((task: CppBuildTask) => { + const command: string = task.definition.command as string; + if (!command) { + return false; + } + if (defaultTemplateConfig.name.startsWith("(Windows) ")) { + if (command.startsWith("cl.exe")) { + return true; + } + } else { + if (!command.startsWith("cl.exe")) { + return true; + } + } + return false; + }); + } + + // Generate new configurations for each build task. + // Generating a task is async, therefore we must *await* *all* map(task => config) Promises to resolve. + let configs: CppDebugConfiguration[] = []; + if (buildTasks.length !== 0) { + configs = (await Promise.all(buildTasks.map>(async task => { + const definition: CppBuildTaskDefinition = task.definition as CppBuildTaskDefinition; + const compilerPath: string = util.isString(definition.command) ? definition.command : definition.command.value; + // Filter out the tasks that has an invalid compiler path. + const compilerPathExists: boolean = path.isAbsolute(compilerPath) ? + // Absolute path, just check if it exists + await util.checkFileExists(compilerPath) : + // Non-absolute. Check on $PATH + (await util.whichAsync(compilerPath) !== undefined); + if (!compilerPathExists) { + getOutputChannelLogger().appendLine(localize('compiler.path.not.exists', "Unable to find {0}. {1} task is ignored.", compilerPath, definition.label)); + } + const compilerName: string = path.basename(compilerPath); + const newConfig: CppDebugConfiguration = { ...defaultTemplateConfig }; // Copy enumerables and properties + newConfig.existing = false; + + newConfig.name = configPrefix + compilerName + " " + this.buildAndDebugActiveFileStr(); + newConfig.preLaunchTask = task.name; + if (newConfig.type === DebuggerType.cppdbg) { + newConfig.externalConsole = false; + } else { + newConfig.console = "integratedTerminal"; + } + // Extract the .exe path from the defined task. + const definedExePath: string | undefined = util.findExePathInArgs(task.definition.args); + newConfig.program = definedExePath ? definedExePath : util.defaultExePath(); + // Add the "detail" property to show the compiler path in QuickPickItem. + // This property will be removed before writing the DebugConfiguration in launch.json. + newConfig.detail = localize("pre.Launch.Task", "preLaunchTask: {0}", task.name); + newConfig.taskDetail = task.detail; + newConfig.taskStatus = task.existing ? + (task.name === DebugConfigurationProvider.recentBuildTaskLabelStr) ? TaskStatus.recentlyUsed : TaskStatus.configured : + TaskStatus.detected; + if (task.isDefault) { + newConfig.isDefault = true; + } + const isCl: boolean = compilerName === "cl.exe"; + newConfig.cwd = isWindows && !isCl && !process.env.PATH?.includes(path.dirname(compilerPath)) ? path.dirname(compilerPath) : "${fileDirname}"; + + if (platformInfo.platform !== "darwin") { + let debuggerName: string; + if (compilerName.startsWith("clang")) { + newConfig.MIMode = "lldb"; + if (isWindows) { + debuggerName = "lldb"; + } else { + debuggerName = "lldb-mi"; + // Search for clang-8, clang-10, etc. + if ((compilerName !== "clang-cl.exe") && (compilerName !== "clang-cpp.exe")) { + const suffixIndex: number = compilerName.indexOf("-"); + if (suffixIndex !== -1) { + const suffix: string = compilerName.substring(suffixIndex); + debuggerName += suffix; + } + } + } + newConfig.type = DebuggerType.cppdbg; + } else if (compilerName === "cl.exe") { + newConfig.miDebuggerPath = undefined; + newConfig.type = DebuggerType.cppvsdbg; + return newConfig; + } else { + debuggerName = "gdb"; + } + if (isWindows) { + debuggerName = debuggerName.endsWith(".exe") ? debuggerName : (debuggerName + ".exe"); + } + 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; + } else { + getOutputChannelLogger().appendLine(localize('debugger.path.not.exists', "Unable to find the {0} debugger. The debug configuration for {1} is ignored.", `\"${debuggerName}\"`, compilerName)); + return undefined; + } + } + } + return newConfig; + }))).filter((item): item is CppDebugConfiguration => !!item); + } + configs.push(defaultTemplateConfig); + const existingConfigs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, type)?.map(config => { + if (!config.detail && config.preLaunchTask) { + config.detail = localize("pre.Launch.Task", "preLaunchTask: {0}", config.preLaunchTask); + } + config.existing = true; + return config; + }); + if (existingConfigs) { + // Remove the detected configs that are already configured once in launch.json. + const dedupExistingConfigs: CppDebugConfiguration[] = configs.filter(detectedConfig => !existingConfigs.some(config => { + if (config.preLaunchTask === detectedConfig.preLaunchTask && config.type === detectedConfig.type && config.request === detectedConfig.request) { + // Carry the default task information. + config.isDefault = detectedConfig.isDefault ? detectedConfig.isDefault : undefined; + return true; + } + return false; + })); + configs = existingConfigs.concat(dedupExistingConfigs); + } + return configs; + } + + private async loadDetectedTasks(): Promise { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + const emptyTasks: CppBuildTask[] = []; + if (!editor) { + DebugConfigurationProvider.detectedBuildTasks = emptyTasks; + return; + } + + const fileExt: string = path.extname(editor.document.fileName); + if (!fileExt) { + DebugConfigurationProvider.detectedBuildTasks = emptyTasks; + return; + } + + // Don't offer tasks for header files. + const isHeader: boolean = util.isHeaderFile(editor.document.uri); + if (isHeader) { + DebugConfigurationProvider.detectedBuildTasks = emptyTasks; + return; + } + + // Don't offer tasks if the active file's extension is not a recognized C/C++ extension. + const fileIsCpp: boolean = util.isCppFile(editor.document.uri); + const fileIsC: boolean = util.isCFile(editor.document.uri); + if (!(fileIsCpp || fileIsC)) { + DebugConfigurationProvider.detectedBuildTasks = emptyTasks; + return; + } + + if (fileIsCpp) { + if (!DebugConfigurationProvider.detectedCppBuildTasks || DebugConfigurationProvider.detectedCppBuildTasks.length === 0) { + DebugConfigurationProvider.detectedCppBuildTasks = await cppBuildTaskProvider.getTasks(true); + } + DebugConfigurationProvider.detectedBuildTasks = DebugConfigurationProvider.detectedCppBuildTasks; + } else { + if (!DebugConfigurationProvider.detectedCBuildTasks || DebugConfigurationProvider.detectedCBuildTasks.length === 0) { + DebugConfigurationProvider.detectedCBuildTasks = await cppBuildTaskProvider.getTasks(true); + } + DebugConfigurationProvider.detectedBuildTasks = DebugConfigurationProvider.detectedCBuildTasks; + } + } + + public static get recentBuildTaskLabelStr(): string { + return DebugConfigurationProvider.recentBuildTaskLabel; + } + + public static set recentBuildTaskLabelStr(recentTask: string) { + DebugConfigurationProvider.recentBuildTaskLabel = recentTask; + } + + private buildAndDebugActiveFileStr(): string { + return `${localize("build.and.debug.active.file", 'build and debug active file')}`; + } + + private isClConfiguration(configurationLabel: string): boolean { + return configurationLabel.startsWith("C/C++: cl.exe"); + } + + private showErrorIfClNotAvailable(_configurationLabel: string): boolean { + if (!process.env.DevEnvDir || process.env.DevEnvDir.length === 0) { + void 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 true; + } + return false; + } + + private getLLDBFrameworkPath(): string | undefined { + const LLDBFramework: string = "LLDB.framework"; + // Note: When adding more search paths, make sure the shipped lldb-mi also has it. See Build/lldb-mi.yml and 'install_name_tool' commands. + const searchPaths: string[] = [ + "/Library/Developer/CommandLineTools/Library/PrivateFrameworks", // XCode CLI + "/Applications/Xcode.app/Contents/SharedFrameworks" // App Store XCode + ]; + + for (const searchPath of searchPaths) { + if (fs.existsSync(path.join(searchPath, LLDBFramework))) { + // Found a framework that 'lldb-mi' can use. + 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: string = "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)); + showOutputChannel(); + + return undefined; + } + + private resolveEnvFile(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): void { + if (config.envFile) { + // replace ${env:???} variables + let envFilePath: string = util.resolveVariables(config.envFile, undefined); + + try { + if (folder && folder.uri && folder.uri.fsPath) { + // Try to replace ${workspaceFolder} or ${workspaceRoot} + envFilePath = envFilePath.replace(/(\${workspaceFolder}|\${workspaceRoot})/g, folder.uri.fsPath); + } + + const parsedFile: ParsedEnvironmentFile = ParsedEnvironmentFile.CreateFromFile(envFilePath, config["environment"]); + + // show error message if single lines cannot get parsed + if (parsedFile.Warning) { + void DebugConfigurationProvider.showFileWarningAsync(parsedFile.Warning, config.envFile); + } + + config.environment = parsedFile.Env; + + delete config.envFile; + } catch (errJS) { + const e: Error = errJS as Error; + throw new Error(localize("envfile.failed", "Failed to use {0}. Reason: {1}", "envFile", e.message)); + } + } + } + + private resolveSourceFileMapVariables(config: CppDebugConfiguration): void { + const messages: string[] = []; + if (config.sourceFileMap) { + for (const sourceFileMapSource of Object.keys(config.sourceFileMap)) { + let message: string = ""; + const sourceFileMapTarget: string = config.sourceFileMap[sourceFileMapSource]; + + let source: string = sourceFileMapSource; + let target: string | object = sourceFileMapTarget; + + // TODO: pass config.environment as 'additionalEnvironment' to resolveVariables when it is { key: value } instead of { "key": key, "value": value } + const newSourceFileMapSource: string = util.resolveVariables(sourceFileMapSource, undefined); + if (sourceFileMapSource !== newSourceFileMapSource) { + message = "\t" + localize("replacing.sourcepath", "Replacing {0} '{1}' with '{2}'.", "sourcePath", sourceFileMapSource, newSourceFileMapSource); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete config.sourceFileMap[sourceFileMapSource]; + source = newSourceFileMapSource; + } + + if (util.isString(sourceFileMapTarget)) { + const newSourceFileMapTarget: string = util.resolveVariables(sourceFileMapTarget, undefined); + if (sourceFileMapTarget !== newSourceFileMapTarget) { + // Add a space if source was changed, else just tab the target message. + message += message ? ' ' : '\t'; + message += localize("replacing.targetpath", "Replacing {0} '{1}' with '{2}'.", "targetPath", sourceFileMapTarget, newSourceFileMapTarget); + target = newSourceFileMapTarget; + } + } else if (util.isObject(sourceFileMapTarget)) { + const newSourceFileMapTarget: { "editorPath": string; "useForBreakpoints": boolean } = sourceFileMapTarget; + newSourceFileMapTarget["editorPath"] = util.resolveVariables(sourceFileMapTarget["editorPath"], undefined); + + if (sourceFileMapTarget !== newSourceFileMapTarget) { + // Add a space if source was changed, else just tab the target message. + 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) { + getOutputChannel().appendLine(localize("resolving.variables.in.sourcefilemap", "Resolving variables in {0}...", "sourceFileMap")); + messages.forEach((message) => { + getOutputChannel().appendLine(message); + }); + showOutputChannel(); + } + } + } + + private static async showFileWarningAsync(message: string, fileName: string): Promise { + const openItem: vscode.MessageItem = { title: localize("open.envfile", "Open {0}", "envFile") }; + const result: vscode.MessageItem | undefined = await vscode.window.showWarningMessage(message, openItem); + if (result && result.title === openItem.title) { + const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(fileName); + if (doc) { + void vscode.window.showTextDocument(doc); + } + } + } + + private localizeConfigDetail(items: ConfigMenu[]): ConfigMenu[] { + items.map((item: ConfigMenu) => { + switch (item.detail) { + case TaskStatus.recentlyUsed: { + item.detail = localize("recently.used.task", "Recently Used Task"); + break; + } + case TaskStatus.configured: { + item.detail = localize("configured.task", "Configured Task"); + break; + } + case TaskStatus.detected: { + item.detail = localize("detected.task", "Detected Task"); + break; + } + default: { + break; + } + } + if (item.configuration.taskDetail) { + // Add the compiler path of the preLaunchTask to the description of the debug configuration. + item.detail = (item.detail ?? "") + " (" + item.configuration.taskDetail + ")"; + } + }); + return items; + } + + private findDefaultConfig(configs: CppDebugConfiguration[]): CppDebugConfiguration[] { + // eslint-disable-next-line no-prototype-builtins + return configs.filter((config: CppDebugConfiguration) => config.hasOwnProperty("isDefault") && config.isDefault); + } + + private async isExistingTask(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): Promise { + if (config.taskStatus && (config.taskStatus !== TaskStatus.detected)) { + return true; + } else if (config.taskStatus && (config.taskStatus === TaskStatus.detected)) { + return false; + } + return cppBuildTaskProvider.isExistingTask(config.preLaunchTask, folder); + } + + private isExistingConfig(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): boolean { + if (config.existing) { + return config.existing; + } + const configs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, config.type); + if (configs && configs.length > 0) { + const selectedConfig: any | undefined = configs.find((item: any) => item.name && item.name === config.name); + if (selectedConfig) { + return true; + } + } + return false; + } + + private getDebugConfigSource(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): ConfigSource | undefined { + if (config.configSource) { + return config.configSource; + } + const isExistingConfig: boolean = this.isExistingConfig(config, folder); + if (!isExistingConfig && !folder) { + return ConfigSource.singleFile; + } else if (!isExistingConfig) { + return ConfigSource.workspaceFolder; + } + + const configs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, config.type); + const matchingConfig: CppDebugConfiguration | undefined = configs?.find((item: any) => item.name && item.name === config.name); + if (matchingConfig?.configSource) { + return matchingConfig.configSource; + } + return ConfigSource.unknown; + } + + public getLaunchConfigs(folder?: vscode.WorkspaceFolder, type?: DebuggerType | string): CppDebugConfiguration[] | undefined { + // Get existing debug configurations from launch.json or user/workspace "launch" settings. + const WorkspaceConfigs: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('launch', folder); + const configs: any = WorkspaceConfigs.inspect('configurations'); + if (!configs) { + return undefined; + } + let detailedConfigs: CppDebugConfiguration[] = []; + if (configs.workspaceFolderValue !== undefined) { + detailedConfigs = detailedConfigs.concat(configs.workspaceFolderValue.map((item: CppDebugConfiguration) => { + item.configSource = ConfigSource.workspaceFolder; + return item; + })); + } + if (configs.workspaceValue !== undefined) { + detailedConfigs = detailedConfigs.concat(configs.workspaceValue.map((item: CppDebugConfiguration) => { + item.configSource = ConfigSource.workspace; + return item; + })); + } + if (configs.globalValue !== undefined) { + detailedConfigs = detailedConfigs.concat(configs.globalValue.map((item: CppDebugConfiguration) => { + item.configSource = ConfigSource.global; + return item; + })); + } + detailedConfigs = detailedConfigs.filter((config: any) => config.name && config.request === "launch" && type ? (config.type === type) : true); + return detailedConfigs; + } + + private getLaunchJsonPath(): string | undefined { + return util.getJsonPath("launch.json"); + } + + private getRawLaunchJson(): Promise { + const path: string | undefined = this.getLaunchJsonPath(); + return util.getRawJson(path); + } + + public async writeDebugConfig(config: vscode.DebugConfiguration, isExistingConfig: boolean, _folder?: vscode.WorkspaceFolder): Promise { + const launchJsonPath: string | undefined = this.getLaunchJsonPath(); + + if (isExistingConfig) { + if (launchJsonPath) { + const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(launchJsonPath); + if (doc) { + void vscode.window.showTextDocument(doc); + } + } + return; + } + const rawLaunchJson: any = await this.getRawLaunchJson(); + if (!rawLaunchJson.configurations) { + rawLaunchJson.configurations = []; + } + if (!rawLaunchJson.version) { + rawLaunchJson.version = "2.0.0"; + } + + // Remove the extra properties that are not a part of the vsCode.DebugConfiguration. + config.detail = undefined; + config.taskStatus = undefined; + config.isDefault = undefined; + config.source = undefined; + config.debuggerEvent = undefined; + config.debugType = undefined; + config.existing = undefined; + config.taskDetail = undefined; + rawLaunchJson.configurations.push(config); + + if (!launchJsonPath) { + throw new Error("Failed to get tasksJsonPath in checkBuildTaskExists()"); + } + + const settings: OtherSettings = new OtherSettings(); + await util.writeFileText(launchJsonPath, jsonc.stringify(rawLaunchJson, null, settings.editorTabSize)); + await vscode.workspace.openTextDocument(launchJsonPath); + const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(launchJsonPath); + if (doc) { + void vscode.window.showTextDocument(doc); + } + } + + public async addDebugConfiguration(textEditor: vscode.TextEditor): Promise { + const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if (!folder) { + return; + } + const selectedConfig: vscode.DebugConfiguration | undefined = await this.selectConfiguration(textEditor, false, true); + if (!selectedConfig) { + Telemetry.logDebuggerEvent(DebuggerEvent.addConfigGear, { "configSource": ConfigSource.workspaceFolder, "configMode": ConfigMode.launchConfig, "cancelled": "true", "succeeded": "true" }); + return; // User canceled it. + } + + const isExistingConfig: boolean = this.isExistingConfig(selectedConfig, folder); + // Write preLaunchTask into tasks.json file. + if (!isExistingConfig && selectedConfig.preLaunchTask && (selectedConfig.taskStatus && selectedConfig.taskStatus === TaskStatus.detected)) { + 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; + // 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" }); + } + + public async buildAndRun(textEditor: vscode.TextEditor): Promise { + // Turn off the debug mode. + return this.buildAndDebug(textEditor, false); + } + + public async buildAndDebug(textEditor: vscode.TextEditor, debugModeOn: boolean = true): Promise { + let folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + const selectedConfig: CppDebugConfiguration | undefined = await this.selectConfiguration(textEditor); + if (!selectedConfig) { + Telemetry.logDebuggerEvent(DebuggerEvent.playButton, { "debugType": debugModeOn ? DebugType.debug : DebugType.run, "configSource": ConfigSource.unknown, "cancelled": "true", "succeeded": "true" }); + return; // User canceled it. + } + + // Keep track of the entry point where the debug has been selected, for telemetry purposes. + selectedConfig.debuggerEvent = DebuggerEvent.playButton; + // If the configs are coming from workspace or global settings and the task is not found in tasks.json, let that to be resolved by VsCode. + if (selectedConfig.preLaunchTask && selectedConfig.configSource && + (selectedConfig.configSource === ConfigSource.global || selectedConfig.configSource === ConfigSource.workspace) && + !await this.isExistingTask(selectedConfig)) { + folder = undefined; + } + selectedConfig.debugType = debugModeOn ? DebugType.debug : DebugType.run; + // startDebugging will trigger a call to resolveDebugConfiguration. + await vscode.debug.startDebugging(folder, selectedConfig, { noDebug: !debugModeOn }); + } + + private async selectConfiguration(textEditor: vscode.TextEditor, pickDefault: boolean = true, onlyWorkspaceFolder: boolean = false): Promise { + const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if (!util.isCppOrCFile(textEditor.document.uri)) { + void vscode.window.showErrorMessage(localize("cannot.build.non.cpp", 'Cannot build and debug because the active file is not a C or C++ source file.')); + return; + } + + // 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)); + } + if (onlyWorkspaceFolder) { + configs = configs.filter(item => !item.configSource || item.configSource === ConfigSource.workspaceFolder); + } + + const defaultConfig: CppDebugConfiguration[] | undefined = pickDefault ? this.findDefaultConfig(configs) : undefined; + + const items: ConfigMenu[] = configs.map(config => ({ label: config.name, configuration: config, description: config.detail, detail: config.taskStatus })); + + let selection: ConfigMenu | undefined; + + // if there was only one config for the default task, choose that config, otherwise ask the user to choose. + if (defaultConfig && defaultConfig.length === 1) { + selection = { label: defaultConfig[0].name, configuration: defaultConfig[0], description: defaultConfig[0].detail, detail: defaultConfig[0].taskStatus }; + } else { + let sortedItems: ConfigMenu[] = []; + // Find the recently used task and place it at the top of quickpick list. + const recentTask: ConfigMenu[] = items.filter(item => item.configuration.preLaunchTask && item.configuration.preLaunchTask === DebugConfigurationProvider.recentBuildTaskLabelStr); + if (recentTask.length !== 0 && recentTask[0].detail !== TaskStatus.detected) { + recentTask[0].detail = TaskStatus.recentlyUsed; + sortedItems.push(recentTask[0]); + } + sortedItems = sortedItems.concat(items.filter(item => item.detail === TaskStatus.configured)); + sortedItems = sortedItems.concat(items.filter(item => item.detail === TaskStatus.detected)); + sortedItems = sortedItems.concat(items.filter(item => item.detail === undefined)); + + selection = await vscode.window.showQuickPick(this.localizeConfigDetail(sortedItems), { + placeHolder: items.length === 0 ? localize("no.compiler.found", "No compiler found") : localize("select.debug.configuration", "Select a debug configuration") + }); + } + if (selection && this.isClConfiguration(selection.configuration.name) && this.showErrorIfClNotAvailable(selection.configuration.name)) { + return; + } + return selection?.configuration; + } + + private async resolvePreLaunchTask(config: CppDebugConfiguration, configMode: ConfigMode, folder?: vscode.WorkspaceFolder | undefined): Promise { + if (config.preLaunchTask) { + try { + if (config.configSource === ConfigSource.singleFile) { + // In case of singleFile, remove the preLaunch task from the debug configuration and run it here instead. + await cppBuildTaskProvider.runBuildTask(config.preLaunchTask); + } else { + await cppBuildTaskProvider.writeDefaultBuildTask(config.preLaunchTask, folder); + } + } catch (errJS) { + const e: Error = errJS as Error; + if (e && e.message === util.failedToParseJson) { + void vscode.window.showErrorMessage(util.failedToParseJson); + } + Telemetry.logDebuggerEvent(config.debuggerEvent || DebuggerEvent.debugPanel, { "debugType": config.debugType || DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": configMode, "cancelled": "false", "succeeded": "false" }); + } + } + } + + private async expand(config: vscode.DebugConfiguration, folder: vscode.WorkspaceFolder | undefined): Promise { + const folderPath: string | undefined = folder?.uri.fsPath || vscode.workspace.workspaceFolders?.[0].uri.fsPath; + const vars: ExpansionVars = config.variables ? config.variables : {}; + vars.workspaceFolder = folderPath || '{workspaceFolder}'; + vars.workspaceFolderBasename = folderPath ? path.basename(folderPath) : '{workspaceFolderBasename}'; + const expansionOptions: ExpansionOptions = { vars, recursive: true }; + return expandAllStrings(config, expansionOptions); + } + + // Returns true when ALL steps succeed; stop all subsequent steps if one fails + private async deploySteps(config: vscode.DebugConfiguration, cancellationToken?: vscode.CancellationToken): Promise { + let succeeded: boolean = true; + const deployStart: number = new Date().getTime(); + + for (const step of config.deploySteps) { + succeeded = await this.singleDeployStep(config, step, cancellationToken); + if (!succeeded) { + break; + } + } + + const deployEnd: number = new Date().getTime(); + + const telemetryProperties: { [key: string]: string } = { + Succeeded: `${succeeded}`, + IsDebugging: `${!config.noDebug || false}` + }; + const telemetryMetrics: { [key: string]: number } = { + NumSteps: config.deploySteps.length, + Duration: deployEnd - deployStart + }; + Telemetry.logDebuggerEvent('deploy', telemetryProperties, telemetryMetrics); + + return succeeded; + } + + private async singleDeployStep(config: vscode.DebugConfiguration, step: any, cancellationToken?: vscode.CancellationToken): Promise { + if ((config.noDebug && step.debug === true) || (!config.noDebug && step.debug === false)) { + // Skip steps that doesn't match current launch mode. Explicit true/false check, since a step is always run when debug is undefined. + return true; + } + const stepType: StepType = step.type; + switch (stepType) { + case StepType.command: { + // VS Code commands are the same regardless of which extension invokes them, so just invoke them here. + if (step.args && !Array.isArray(step.args)) { + void getOutputChannelLogger().showErrorMessage(localize('command.args.must.be.array', '"args" in command deploy step must be an array.')); + return false; + } + const returnCode: unknown = await vscode.commands.executeCommand(step.command, ...step.args); + return !returnCode; + } + case StepType.scp: + case StepType.rsync: { + const isScp: boolean = stepType === StepType.scp; + if (!step.files || !step.targetDir || !step.host) { + void getOutputChannelLogger().showErrorMessage(localize('missing.properties.copyFile', '"host", "files", and "targetDir" are required in {0} steps.', isScp ? 'SCP' : 'rsync')); + return false; + } + const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port }; + const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts; + let files: vscode.Uri[] = []; + if (util.isString(step.files)) { + files = files.concat((await globAsync(step.files)).map(file => vscode.Uri.file(file))); + } else if (util.isArrayOfString(step.files)) { + for (const fileGlob of (step.files as string[])) { + files = files.concat((await globAsync(fileGlob)).map(file => vscode.Uri.file(file))); + } + } else { + void getOutputChannelLogger().showErrorMessage(localize('incorrect.files.type.copyFile', '"files" must be a string or an array of strings in {0} steps.', isScp ? 'SCP' : 'rsync')); + return false; + } + + let scpResult: util.ProcessReturnType; + if (isScp) { + scpResult = await scp(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); + } else { + scpResult = await rsync(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); + } + + if (!scpResult.succeeded || cancellationToken?.isCancellationRequested) { + return false; + } + break; + } + case StepType.ssh: { + if (!step.host || !step.command) { + void getOutputChannelLogger().showErrorMessage(localize('missing.properties.ssh', '"host" and "command" are required for ssh steps.')); + return false; + } + const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port }; + 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); + if (!sshResult.succeeded || cancellationToken?.isCancellationRequested) { + return false; + } + break; + } + case StepType.shell: { + if (!step.command) { + void 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); + if (!taskResult.succeeded || cancellationToken?.isCancellationRequested) { + void getOutputChannelLogger().showErrorMessage(taskResult.output); + return false; + } + break; + } + default: { + getOutputChannelLogger().appendLine(localize('deploy.step.type.not.supported', 'Deploy step type {0} is not supported.', step.type)); + return false; + } + } + return true; + } +} + +export interface IConfigurationAssetProvider { + getInitialConfigurations(debuggerType: DebuggerType): any; + getConfigurationSnippets(): vscode.CompletionItem[]; +} + +export class ConfigurationAssetProviderFactory { + public static getConfigurationProvider(): IConfigurationAssetProvider { + 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: 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 { + 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 convertConfigurationSnippetToCompletionItem(snippet: IConfigurationSnippet): vscode.CompletionItem { + const item: vscode.CompletionItem = new vscode.CompletionItem(snippet.label, vscode.CompletionItemKind.Module); + + item.insertText = snippet.bodyText; + + return item; +} + +export class ConfigurationSnippetProvider implements vscode.CompletionItemProvider { + private provider: IConfigurationAssetProvider; + private snippets: vscode.CompletionItem[]; + + constructor(provider: IConfigurationAssetProvider) { + this.provider = provider; + this.snippets = this.provider.getConfigurationSnippets(); + } + public resolveCompletionItem(item: vscode.CompletionItem, _token: vscode.CancellationToken): Thenable { + return Promise.resolve(item); + } + + // This function will only provide completion items via the Add Configuration Button + // There are two cases where the configuration array has nothing or has some items. + // 1. If it has nothing, insert a snippet the user selected. + // 2. If there are items, the Add Configuration button will append it to the start of the configuration array. This function inserts a comma at the end of the snippet. + public provideCompletionItems(document: vscode.TextDocument, _position: vscode.Position, _token: vscode.CancellationToken, _context: vscode.CompletionContext): Thenable { + let items: vscode.CompletionItem[] = this.snippets; + let hasLaunchConfigs: boolean = false; + try { + const launch: any = jsonc.parse(document.getText()); + hasLaunchConfigs = launch.configurations.length !== 0; + } catch { + // ignore + } + + // Check to see if the array is empty, so any additional inserted snippets will need commas. + if (hasLaunchConfigs) { + items = []; + + // Make a copy of each snippet since we are adding a comma to the end of the insertText. + this.snippets.forEach((item) => items.push({ ...item })); + + items.map((item) => { + item.insertText = item.insertText + ','; // Add comma + }); + } + + return Promise.resolve(new vscode.CompletionList(items, true)); + } +} diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index ba353a858a..3130ec0d63 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -38,6 +38,7 @@ import { logAndReturn } from '../Utility/Async/returns'; import { is } from '../Utility/System/guards'; import * as util from '../common'; import { isWindows } from '../constants'; +import { instrument, isInstrumentationEnabled } from '../instrumentation'; import { DebugProtocolParams, Logger, ShowWarningParams, getDiagnosticsChannel, getOutputChannelLogger, logDebugProtocol, logLocalized, showWarning } from '../logger'; import { localizedStringCount, lookupString } from '../nativeStrings'; import { SessionState } from '../sessionState'; @@ -810,7 +811,16 @@ export interface Client { } export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { - return new DefaultClient(workspaceFolder); + if (isInstrumentationEnabled()) { + instrument(vscode.languages, { name: "languages" }); + instrument(vscode.window, { name: "window" }); + instrument(vscode.workspace, { name: "workspace" }); + instrument(vscode.commands, { name: "commands" }); + instrument(vscode.debug, { name: "debug" }); + instrument(vscode.env, { name: "env" }); + instrument(vscode.extensions, { name: "extensions" }); + } + return instrument(new DefaultClient(workspaceFolder), { ignore: ["enqueue", "onInterval", "logTelemetry"] }); } export function createNullClient(): Client { @@ -1274,27 +1284,27 @@ export class DefaultClient implements Client { initializedClientCount = 0; this.inlayHintsProvider = new InlayHintsProvider(); - this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, new HoverProvider(this))); - this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, this.inlayHintsProvider)); - this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, new RenameProvider(this))); - this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, new FindAllReferencesProvider(this))); - this.disposables.push(vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this))); - this.disposables.push(vscode.languages.registerDocumentSymbolProvider(util.documentSelector, new DocumentSymbolProvider(), undefined)); - this.disposables.push(vscode.languages.registerCodeActionsProvider(util.documentSelector, new CodeActionProvider(this), undefined)); - this.disposables.push(vscode.languages.registerCallHierarchyProvider(util.documentSelector, new CallHierarchyProvider(this))); + this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, instrument(new HoverProvider(this)))); + this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, instrument(this.inlayHintsProvider))); + this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, instrument(new RenameProvider(this)))); + this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, instrument(new FindAllReferencesProvider(this)))); + this.disposables.push(vscode.languages.registerWorkspaceSymbolProvider(instrument(new WorkspaceSymbolProvider(this)))); + this.disposables.push(vscode.languages.registerDocumentSymbolProvider(util.documentSelector, instrument(new DocumentSymbolProvider()), undefined)); + this.disposables.push(vscode.languages.registerCodeActionsProvider(util.documentSelector, instrument(new CodeActionProvider(this)), undefined)); + this.disposables.push(vscode.languages.registerCallHierarchyProvider(util.documentSelector, instrument(new CallHierarchyProvider(this)))); // Because formatting and codeFolding can vary per folder, we need to register these providers once // and leave them registered. The decision of whether to provide results needs to be made on a per folder basis, // within the providers themselves. - this.documentFormattingProviderDisposable = vscode.languages.registerDocumentFormattingEditProvider(util.documentSelector, new DocumentFormattingEditProvider(this)); - this.formattingRangeProviderDisposable = vscode.languages.registerDocumentRangeFormattingEditProvider(util.documentSelector, new DocumentRangeFormattingEditProvider(this)); - this.onTypeFormattingProviderDisposable = vscode.languages.registerOnTypeFormattingEditProvider(util.documentSelector, new OnTypeFormattingEditProvider(this), ";", "}", "\n"); + this.documentFormattingProviderDisposable = vscode.languages.registerDocumentFormattingEditProvider(util.documentSelector, instrument(new DocumentFormattingEditProvider(this))); + this.formattingRangeProviderDisposable = vscode.languages.registerDocumentRangeFormattingEditProvider(util.documentSelector, instrument(new DocumentRangeFormattingEditProvider(this))); + this.onTypeFormattingProviderDisposable = vscode.languages.registerOnTypeFormattingEditProvider(util.documentSelector, instrument(new OnTypeFormattingEditProvider(this)), ";", "}", "\n"); this.codeFoldingProvider = new FoldingRangeProvider(this); - this.codeFoldingProviderDisposable = vscode.languages.registerFoldingRangeProvider(util.documentSelector, this.codeFoldingProvider); + this.codeFoldingProviderDisposable = vscode.languages.registerFoldingRangeProvider(util.documentSelector, instrument(this.codeFoldingProvider)); const settings: CppSettings = new CppSettings(); if (settings.isEnhancedColorizationEnabled && semanticTokensLegend) { - this.semanticTokensProvider = new SemanticTokensProvider(); + this.semanticTokensProvider = instrument(new SemanticTokensProvider()); this.semanticTokensProviderDisposable = vscode.languages.registerDocumentSemanticTokensProvider(util.documentSelector, this.semanticTokensProvider, semanticTokensLegend); } @@ -1652,8 +1662,7 @@ export class DefaultClient implements Client { const oldLoggingLevelLogged: boolean = this.loggingLevel > 1; this.loggingLevel = util.getNumericLoggingLevel(changedSettings.loggingLevel); if (oldLoggingLevelLogged || this.loggingLevel > 1) { - const out: Logger = getOutputChannelLogger(); - out.appendLine(localize({ key: "loggingLevel.changed", comment: ["{0} is the setting name 'loggingLevel', {1} is a string value such as 'Debug'"] }, "{0} has changed to: {1}", "loggingLevel", changedSettings.loggingLevel)); + getOutputChannelLogger().appendLine(localize({ key: "loggingLevel.changed", comment: ["{0} is the setting name 'loggingLevel', {1} is a string value such as 'Debug'"] }, "{0} has changed to: {1}", "loggingLevel", changedSettings.loggingLevel)); } } const settings: CppSettings = new CppSettings(); @@ -2653,12 +2662,7 @@ export class DefaultClient implements Client { const status: IntelliSenseStatus = { status: Status.IntelliSenseCompiling }; testHook.updateStatus(status); } else if (message.endsWith("IntelliSense done")) { - const settings: CppSettings = new CppSettings(); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - const out: Logger = getOutputChannelLogger(); - const duration: number = Date.now() - timeStamp; - out.appendLine(localize("update.intellisense.time", "Update IntelliSense time (sec): {0}", duration / 1000)); - } + getOutputChannelLogger().appendLine(6, localize("update.intellisense.time", "Update IntelliSense time (sec): {0}", (Date.now() - timeStamp) / 1000)); this.model.isUpdatingIntelliSense.Value = false; const status: IntelliSenseStatus = { status: Status.IntelliSenseReady }; testHook.updateStatus(status); @@ -3084,11 +3088,8 @@ export class DefaultClient implements Client { return; } - const settings: CppSettings = new CppSettings(); const out: Logger = getOutputChannelLogger(); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - out.appendLine(localize("configurations.received", "Custom configurations received:")); - } + out.appendLine(6, localize("configurations.received", "Custom configurations received:")); const sanitized: SourceFileConfigurationItemAdapter[] = []; configs.forEach(item => { if (this.isSourceFileConfigurationItem(item, providerVersion)) { @@ -3100,10 +3101,8 @@ export class DefaultClient implements Client { uri = item.uri.toString(); } this.configurationLogging.set(uri, JSON.stringify(item.configuration, null, 4)); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - out.appendLine(` uri: ${uri}`); - out.appendLine(` config: ${JSON.stringify(item.configuration, null, 2)}`); - } + out.appendLine(6, ` uri: ${uri}`); + out.appendLine(6, ` config: ${JSON.stringify(item.configuration, null, 2)}`); if (item.configuration.includePath.some(path => path.endsWith('**'))) { console.warn("custom include paths should not use recursive includes ('**')"); } @@ -3207,11 +3206,7 @@ export class DefaultClient implements Client { return; } - const settings: CppSettings = new CppSettings(); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - const out: Logger = getOutputChannelLogger(); - out.appendLine(localize("browse.configuration.received", "Custom browse configuration received: {0}", JSON.stringify(sanitized, null, 2))); - } + getOutputChannelLogger().appendLine(6, localize("browse.configuration.received", "Custom browse configuration received: {0}", JSON.stringify(sanitized, null, 2))); // Separate compiler path and args before sending to language client if (util.isString(sanitized.compilerPath)) { diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 8bc64f82f8..ae1487025f 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1259,10 +1259,8 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr if (crashCallStack !== prevCppCrashCallStackData) { prevCppCrashCallStackData = crashCallStack; - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - if (lines.length >= 6 && util.getNumericLoggingLevel(settings.get("loggingLevel")) >= 1) { - const out: vscode.OutputChannel = getCrashCallStacksChannel(); - out.appendLine(`\n${isCppToolsSrv ? "cpptools-srv" : "cpptools"}\n${crashDate.toLocaleString()}\n${signalType}${crashCallStack}`); + if (lines.length >= 6 && util.getLoggingLevel() >= 1) { + getCrashCallStacksChannel().appendLine(`\n${isCppToolsSrv ? "cpptools-srv" : "cpptools"}\n${crashDate.toLocaleString()}\n${signalType}${crashCallStack}`); } } diff --git a/Extension/src/LanguageServer/lmTool.ts b/Extension/src/LanguageServer/lmTool.ts index 746ff3829f..8e18592c5a 100644 --- a/Extension/src/LanguageServer/lmTool.ts +++ b/Extension/src/LanguageServer/lmTool.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { localize } from 'vscode-nls'; import * as util from '../common'; -import * as logger from '../logger'; +import { getOutputChannelLogger } from '../logger'; import * as telemetry from '../telemetry'; import { ChatContextResult, ProjectContextResult } from './client'; import { getClients } from './extension'; @@ -171,7 +171,7 @@ export async function getProjectContext(uri: vscode.Uri, context: { flags: Recor catch (exception) { try { const err: Error = exception as Error; - logger.getOutputChannelLogger().appendLine(localize("copilot.projectcontext.error", "Error while retrieving the project context. Reason: {0}", err.message)); + getOutputChannelLogger().appendLine(localize("copilot.projectcontext.error", "Error while retrieving the project context. Reason: {0}", err.message)); } catch { // Intentionally swallow any exception. @@ -239,7 +239,7 @@ export class CppConfigurationLanguageModelTool implements vscode.LanguageModelTo private async reportError(): Promise { try { - logger.getOutputChannelLogger().appendLine(localize("copilot.cppcontext.error", "Error while retrieving the #cpp context.")); + getOutputChannelLogger().appendLine(localize("copilot.cppcontext.error", "Error while retrieving the #cpp context.")); } catch { // Intentionally swallow any exception. diff --git a/Extension/src/LanguageServer/references.ts b/Extension/src/LanguageServer/references.ts index 0fc3d2fbb6..ba541c1c64 100644 --- a/Extension/src/LanguageServer/references.ts +++ b/Extension/src/LanguageServer/references.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { Position, TextDocumentIdentifier } from 'vscode-languageclient'; import * as nls from 'vscode-nls'; import * as util from '../common'; -import * as logger from '../logger'; +import { getOutputChannel } from '../logger'; import * as telemetry from '../telemetry'; import { DefaultClient } from './client'; import { PersistentState } from './persistentState'; @@ -478,7 +478,7 @@ export class ReferencesManager { this.referencesChannel.show(true); } } else if (this.client.ReferencesCommandMode === ReferencesCommandMode.Find) { - const logChannel: vscode.OutputChannel = logger.getOutputChannel(); + const logChannel: vscode.OutputChannel = getOutputChannel(); logChannel.appendLine(msg); logChannel.appendLine(""); logChannel.show(true); diff --git a/Extension/src/common.ts b/Extension/src/common.ts index dbd0bcdd63..d2cf7b513b 100644 --- a/Extension/src/common.ts +++ b/Extension/src/common.ts @@ -1,1820 +1,1817 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - -import * as assert from 'assert'; -import * as child_process from 'child_process'; -import * as jsonc from 'comment-json'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import * as tmp from 'tmp'; -import * as vscode from 'vscode'; -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 { isWindows } from './constants'; -import { getOutputChannelLogger, showOutputChannel } from './logger'; -import { PlatformInformation } from './platform'; -import * as Telemetry from './telemetry'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); -export const failedToParseJson: string = localize("failed.to.parse.json", "Failed to parse json file, possibly due to comments or trailing commas."); - -export type Mutable = { - // eslint-disable-next-line @typescript-eslint/array-type - -readonly [P in keyof T]: T[P] extends ReadonlyArray ? Mutable[] : Mutable -}; - -export let extensionPath: string; -export let extensionContext: vscode.ExtensionContext | undefined; -export function setExtensionContext(context: vscode.ExtensionContext): void { - extensionContext = context; - extensionPath = extensionContext.extensionPath; -} - -export function setExtensionPath(path: string): void { - extensionPath = path; -} - -let cachedClangFormatPath: string | undefined; -export function getCachedClangFormatPath(): string | undefined { - return cachedClangFormatPath; -} - -export function setCachedClangFormatPath(path: string): void { - cachedClangFormatPath = path; -} - -let cachedClangTidyPath: string | undefined; -export function getCachedClangTidyPath(): string | undefined { - return cachedClangTidyPath; -} - -export function setCachedClangTidyPath(path: string): void { - cachedClangTidyPath = path; -} - -// Use this package.json to read values -export const packageJson: any = vscode.extensions.getExtension("ms-vscode.cpptools")?.packageJSON; - -// Use getRawSetting to get subcategorized settings from package.json. -// This prevents having to iterate every time we search. -let flattenedPackageJson: Map; -export function getRawSetting(key: string, breakIfMissing: boolean = false): any { - if (flattenedPackageJson === undefined) { - flattenedPackageJson = new Map(); - for (const subheading of packageJson.contributes.configuration) { - for (const setting in subheading.properties) { - flattenedPackageJson.set(setting, subheading.properties[setting]); - } - } - } - const result = flattenedPackageJson.get(key); - if (result === undefined && breakIfMissing) { - // eslint-disable-next-line no-debugger - debugger; // The setting does not exist in package.json. Check the `key`. - } - return result; -} - -export async function getRawJson(path: string | undefined): Promise { - if (!path) { - return {}; - } - const fileExists: boolean = await checkFileExists(path); - if (!fileExists) { - return {}; - } - - const fileContents: string = await readFileText(path); - let rawElement: any = {}; - try { - rawElement = jsonc.parse(fileContents, undefined, true); - } catch (error) { - throw new Error(failedToParseJson); - } - return rawElement; -} - -// This function is used to stringify the rawPackageJson. -// Do not use with util.packageJson or else the expanded -// package.json will be written back. -export function stringifyPackageJson(packageJson: string): string { - return JSON.stringify(packageJson, null, 2); -} - -export function getExtensionFilePath(extensionfile: string): string { - return path.resolve(extensionPath, extensionfile); -} - -export function getPackageJsonPath(): string { - return getExtensionFilePath("package.json"); -} - -export function getJsonPath(jsonFilaName: string, workspaceFolder?: vscode.WorkspaceFolder): string | undefined { - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!editor) { - return undefined; - } - const folder: vscode.WorkspaceFolder | undefined = workspaceFolder ? workspaceFolder : vscode.workspace.getWorkspaceFolder(editor.document.uri); - if (!folder) { - return undefined; - } - return path.join(folder.uri.fsPath, ".vscode", jsonFilaName); -} - -export function getVcpkgPathDescriptorFile(): string { - if (process.platform === 'win32') { - const pathPrefix: string | undefined = process.env.LOCALAPPDATA; - if (!pathPrefix) { - throw new Error("Unable to read process.env.LOCALAPPDATA"); - } - return path.join(pathPrefix, "vcpkg/vcpkg.path.txt"); - } else { - const pathPrefix: string = os.homedir(); - return path.join(pathPrefix, ".vcpkg/vcpkg.path.txt"); - } -} - -let vcpkgRoot: string | undefined; -export function getVcpkgRoot(): string { - if (!vcpkgRoot && vcpkgRoot !== "") { - vcpkgRoot = ""; - // Check for vcpkg instance. - if (fs.existsSync(getVcpkgPathDescriptorFile())) { - let vcpkgRootTemp: string = fs.readFileSync(getVcpkgPathDescriptorFile()).toString(); - vcpkgRootTemp = vcpkgRootTemp.trim(); - if (fs.existsSync(vcpkgRootTemp)) { - vcpkgRoot = path.join(vcpkgRootTemp, "/installed").replace(/\\/g, "/"); - } - } - } - return vcpkgRoot; -} - -/** - * This is a fuzzy determination of whether a uri represents a header file. - * For the purposes of this function, a header file has no extension, or an extension that begins with the letter 'h'. - * @param document The document to check. - */ -export function isHeaderFile(uri: vscode.Uri): boolean { - const fileExt: string = path.extname(uri.fsPath); - const fileExtLower: string = fileExt.toLowerCase(); - return !fileExt || [".cuh", ".hpp", ".hh", ".hxx", ".h++", ".hp", ".h", ".ii", ".inl", ".idl", ""].some(ext => fileExtLower === ext); -} - -export function isCppFile(uri: vscode.Uri): boolean { - const fileExt: string = path.extname(uri.fsPath); - const fileExtLower: string = fileExt.toLowerCase(); - return (fileExt === ".C") || [".cu", ".cpp", ".cc", ".cxx", ".c++", ".cp", ".ino", ".ipp", ".tcc"].some(ext => fileExtLower === ext); -} - -export function isCFile(uri: vscode.Uri): boolean { - const fileExt: string = path.extname(uri.fsPath); - const fileExtLower: string = fileExt.toLowerCase(); - return (fileExt === ".C") || fileExtLower === ".c"; -} - -export function isCppOrCFile(uri: vscode.Uri | undefined): boolean { - if (!uri) { - return false; - } - return isCppFile(uri) || isCFile(uri); -} - -export function isFolderOpen(uri: vscode.Uri): boolean { - const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(uri); - return folder ? true : false; -} - -export function isEditorFileCpp(file: string): boolean { - const editor: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.find(e => e.document.uri.toString() === file); - if (!editor) { - return false; - } - return editor.document.languageId === "cpp"; -} - -// If it's C, C++, or Cuda. -export function isCpp(document: vscode.TextDocument): boolean { - return document.uri.scheme === "file" && - (document.languageId === "c" || document.languageId === "cpp" || document.languageId === "cuda-cpp"); -} - -export function isCppPropertiesJson(document: vscode.TextDocument): boolean { - return document.uri.scheme === "file" && (document.languageId === "json" || document.languageId === "jsonc") && - document.fileName.endsWith("c_cpp_properties.json"); -} -let isWorkspaceCpp: boolean = false; -export function setWorkspaceIsCpp(): void { - if (!isWorkspaceCpp) { - isWorkspaceCpp = true; - } -} - -export function getWorkspaceIsCpp(): boolean { - return isWorkspaceCpp; -} - -export function isCppOrRelated(document: vscode.TextDocument): boolean { - return isCpp(document) || isCppPropertiesJson(document) || (document.uri.scheme === "output" && document.uri.fsPath.startsWith("extension-output-ms-vscode.cpptools")) || - (isWorkspaceCpp && (document.languageId === "json" || document.languageId === "jsonc") && - ((document.fileName.endsWith("settings.json") && (document.uri.scheme === "file" || document.uri.scheme === "vscode-userdata")) || - (document.uri.scheme === "file" && document.fileName.endsWith(".code-workspace")))); -} - -let isExtensionNotReadyPromptDisplayed: boolean = false; -export const extensionNotReadyString: string = localize("extension.not.ready", 'The C/C++ extension is still installing. See the output window for more information.'); - -export function displayExtensionNotReadyPrompt(): void { - if (!isExtensionNotReadyPromptDisplayed) { - isExtensionNotReadyPromptDisplayed = true; - showOutputChannel(); - - void getOutputChannelLogger().showInformationMessage(extensionNotReadyString).then( - () => { isExtensionNotReadyPromptDisplayed = false; }, - () => { isExtensionNotReadyPromptDisplayed = false; } - ); - } -} - -// This Progress global state tracks how far users are able to get before getting blocked. -// Users start with a progress of 0 and it increases as they get further along in using the tool. -// This eliminates noise/problems due to re-installs, terminated installs that don't send errors, -// errors followed by workarounds that lead to success, etc. -const progressInstallSuccess: number = 100; -const progressExecutableStarted: number = 150; -const progressExecutableSuccess: number = 200; -const progressParseRootSuccess: number = 300; -const progressIntelliSenseNoSquiggles: number = 1000; -// Might add more IntelliSense progress measurements later. -// IntelliSense progress is separate from the install progress, because parse root can occur afterwards. - -const installProgressStr: string = "CPP." + packageJson.version + ".Progress"; -const intelliSenseProgressStr: string = "CPP." + packageJson.version + ".IntelliSenseProgress"; - -export function getProgress(): number { - return extensionContext ? extensionContext.globalState.get(installProgressStr, -1) : -1; -} - -export function getIntelliSenseProgress(): number { - return extensionContext ? extensionContext.globalState.get(intelliSenseProgressStr, -1) : -1; -} - -export function setProgress(progress: number): void { - if (extensionContext && getProgress() < progress) { - void extensionContext.globalState.update(installProgressStr, progress); - const telemetryProperties: Record = {}; - let progressName: string | undefined; - switch (progress) { - case 0: progressName = "install started"; break; - case progressInstallSuccess: progressName = "install succeeded"; break; - case progressExecutableStarted: progressName = "executable started"; break; - case progressExecutableSuccess: progressName = "executable succeeded"; break; - case progressParseRootSuccess: progressName = "parse root succeeded"; break; - } - if (progressName) { - telemetryProperties.progress = progressName; - } - Telemetry.logDebuggerEvent("progress", telemetryProperties); - } -} - -export function setIntelliSenseProgress(progress: number): void { - if (extensionContext && getIntelliSenseProgress() < progress) { - void extensionContext.globalState.update(intelliSenseProgressStr, progress); - const telemetryProperties: Record = {}; - let progressName: string | undefined; - switch (progress) { - case progressIntelliSenseNoSquiggles: progressName = "IntelliSense no squiggles"; break; - } - if (progressName) { - telemetryProperties.progress = progressName; - } - Telemetry.logDebuggerEvent("progress", telemetryProperties); - } -} - -export function getProgressInstallSuccess(): number { return progressInstallSuccess; } // Download/install was successful (i.e. not blocked by component acquisition). -export function getProgressExecutableStarted(): number { return progressExecutableStarted; } // The extension was activated and starting the executable was attempted. -export function getProgressExecutableSuccess(): number { return progressExecutableSuccess; } // Starting the exe was successful (i.e. not blocked by 32-bit or glibc < 2.18 on Linux) -export function getProgressParseRootSuccess(): number { return progressParseRootSuccess; } // Parse root was successful (i.e. not blocked by processing taking too long). -export function getProgressIntelliSenseNoSquiggles(): number { return progressIntelliSenseNoSquiggles; } // IntelliSense was successful and the user got no squiggles. - -export function isUri(input: any): input is vscode.Uri { - return input && input instanceof vscode.Uri; -} - -export function isString(input: any): input is string { - return typeof input === "string"; -} - -export function isNumber(input: any): input is number { - return typeof input === "number"; -} - -export function isBoolean(input: any): input is boolean { - return typeof input === "boolean"; -} - -export function isObject(input: any): boolean { - return input !== null && typeof input === "object" && !isArray(input); -} - -export function isArray(input: any): input is any[] { - return Array.isArray(input); -} - -export function isOptionalString(input: any): input is string | undefined { - return input === undefined || isString(input); -} - -export function isArrayOfString(input: any): input is string[] { - return isArray(input) && input.every(isString); -} - -// Validates whether the given object is a valid mapping of key and value type. -// EX: {"key": true, "key2": false} should return true for keyType = string and valueType = boolean. -export function isValidMapping(value: any, isValidKey: (key: any) => boolean, isValidValue: (value: any) => boolean): value is object { - if (isObject(value)) { - return Object.entries(value).every(([key, val]) => isValidKey(key) && isValidValue(val)); - } - return false; -} - -export function isOptionalArrayOfString(input: any): input is string[] | undefined { - return input === undefined || isArrayOfString(input); -} - -export function resolveCachePath(input: string | undefined, additionalEnvironment: Record): string { - let resolvedPath: string = ""; - if (!input || input.trim() === "") { - // If no path is set, return empty string to language service process, where it will set the default path as - // Windows: %LocalAppData%/Microsoft/vscode-cpptools/ - // Linux and Mac: ~/.vscode-cpptools/ - return resolvedPath; - } - - resolvedPath = resolveVariables(input, additionalEnvironment); - return resolvedPath; -} - -export function defaultExePath(): string { - const exePath: string = path.join('${fileDirname}', '${fileBasenameNoExtension}'); - return isWindows ? exePath + '.exe' : exePath; -} - -// Pass in 'arrayResults' if a string[] result is possible and a delimited string result is undesirable. -// The string[] result will be copied into 'arrayResults'. -export function resolveVariables(input: string | undefined, additionalEnvironment?: Record, arrayResults?: string[]): string { - if (!input) { - return ""; - } - - // jsonc parser may assign a non-string object to a string. - // TODO: https://github.com/microsoft/vscode-cpptools/issues/9414 - if (!isString(input)) { - const inputAny: any = input; - input = inputAny.toString(); - return input ?? ""; - } - - // Replace environment and configuration variables. - const regexp: () => RegExp = () => /\$\{((env|config|workspaceFolder)(\.|:))?(.*?)\}/g; - let ret: string = input; - const cycleCache = new Set(); - while (!cycleCache.has(ret)) { - cycleCache.add(ret); - ret = ret.replace(regexp(), (match: string, ignored1: string, varType: string, ignored2: string, name: string) => { - // Historically, if the variable didn't have anything before the "." or ":" - // it was assumed to be an environment variable - if (!varType) { - varType = "env"; - } - let newValue: string | undefined; - switch (varType) { - case "env": { - if (additionalEnvironment) { - const v: string | string[] | undefined = additionalEnvironment[name]; - if (isString(v)) { - newValue = v; - } else if (input === match && isArrayOfString(v)) { - if (arrayResults !== undefined) { - arrayResults.push(...v); - newValue = ""; - break; - } else { - newValue = v.join(path.delimiter); - } - } - } - if (newValue === undefined) { - newValue = process.env[name]; - } - break; - } - case "config": { - const config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(); - if (config) { - newValue = config.get(name); - } - break; - } - case "workspaceFolder": { - // Only replace ${workspaceFolder:name} variables for now. - // We may consider doing replacement of ${workspaceFolder} here later, but we would have to update the language server and also - // intercept messages with paths in them and add the ${workspaceFolder} variable back in (e.g. for light bulb suggestions) - if (name && vscode.workspace && vscode.workspace.workspaceFolders) { - const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.workspaceFolders.find(folder => folder.name.toLocaleLowerCase() === name.toLocaleLowerCase()); - if (folder) { - newValue = folder.uri.fsPath; - } - } - break; - } - default: { assert.fail("unknown varType matched"); } - } - return newValue !== undefined ? newValue : match; - }); - } - - return resolveHome(ret); -} - -export function resolveVariablesArray(variables: string[] | undefined, additionalEnvironment?: Record): string[] { - let result: string[] = []; - if (variables) { - variables.forEach(variable => { - const variablesResolved: string[] = []; - const variableResolved: string = resolveVariables(variable, additionalEnvironment, variablesResolved); - result = result.concat(variablesResolved.length === 0 ? variableResolved : variablesResolved); - }); - } - return result; -} - -// Resolve '~' at the start of the path. -export function resolveHome(filePath: string): string { - return filePath.replace(/^\~/g, os.homedir()); -} - -export function asFolder(uri: vscode.Uri): string { - let result: string = uri.toString(); - if (!result.endsWith('/')) { - result += '/'; - } - return result; -} - -/** - * get the default open command for the current platform - */ -export function getOpenCommand(): string { - if (os.platform() === 'win32') { - return 'explorer'; - } else if (os.platform() === 'darwin') { - return '/usr/bin/open'; - } else { - return '/usr/bin/xdg-open'; - } -} - -export function getDebugAdaptersPath(file: string): string { - return path.resolve(getExtensionFilePath("debugAdapters"), file); -} - -export async function fsStat(filePath: fs.PathLike): Promise { - let stats: fs.Stats | undefined; - try { - stats = await fs.promises.stat(filePath); - } catch (e) { - // File doesn't exist - return undefined; - } - return stats; -} - -export async function checkPathExists(filePath: string): Promise { - return !!await fsStat(filePath); -} - -/** Test whether a file exists */ -export async function checkFileExists(filePath: string): Promise { - const stats: fs.Stats | undefined = await fsStat(filePath); - return !!stats && stats.isFile(); -} - -/** Test whether a file exists */ -export async function checkExecutableWithoutExtensionExists(filePath: string): Promise { - if (await checkFileExists(filePath)) { - return true; - } - if (os.platform() === 'win32') { - if (filePath.length > 4) { - const possibleExtension: string = filePath.substring(filePath.length - 4).toLowerCase(); - if (possibleExtension === ".exe" || possibleExtension === ".cmd" || possibleExtension === ".bat") { - return false; - } - } - if (await checkFileExists(filePath + ".exe")) { - return true; - } - if (await checkFileExists(filePath + ".cmd")) { - return true; - } - if (await checkFileExists(filePath + ".bat")) { - return true; - } - } - return false; -} - -/** Test whether a directory exists */ -export async function checkDirectoryExists(dirPath: string): Promise { - const stats: fs.Stats | undefined = await fsStat(dirPath); - return !!stats && stats.isDirectory(); -} - -export function createDirIfNotExistsSync(filePath: string | undefined): void { - if (!filePath) { - return; - } - const dirPath: string = path.dirname(filePath); - if (!checkDirectoryExistsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } -} - -export function checkFileExistsSync(filePath: string): boolean { - try { - return fs.statSync(filePath).isFile(); - } catch (e) { - return false; - } -} - -export function checkExecutableWithoutExtensionExistsSync(filePath: string): boolean { - if (checkFileExistsSync(filePath)) { - return true; - } - if (os.platform() === 'win32') { - if (filePath.length > 4) { - const possibleExtension: string = filePath.substring(filePath.length - 4).toLowerCase(); - if (possibleExtension === ".exe" || possibleExtension === ".cmd" || possibleExtension === ".bat") { - return false; - } - } - if (checkFileExistsSync(filePath + ".exe")) { - return true; - } - if (checkFileExistsSync(filePath + ".cmd")) { - return true; - } - if (checkFileExistsSync(filePath + ".bat")) { - return true; - } - } - return false; -} - -/** Test whether a directory exists */ -export function checkDirectoryExistsSync(dirPath: string): boolean { - try { - return fs.statSync(dirPath).isDirectory(); - } catch (e) { - return false; - } -} - -/** Test whether a relative path exists */ -export function checkPathExistsSync(path: string, relativePath: string, _isWindows: boolean, isCompilerPath: boolean): { pathExists: boolean; path: string } { - let pathExists: boolean = true; - const existsWithExeAdded: (path: string) => boolean = (path: string) => isCompilerPath && _isWindows && fs.existsSync(path + ".exe"); - if (!fs.existsSync(path)) { - if (existsWithExeAdded(path)) { - path += ".exe"; - } else if (!relativePath) { - pathExists = false; - } else { - // Check again for a relative path. - relativePath = relativePath + path; - if (!fs.existsSync(relativePath)) { - if (existsWithExeAdded(path)) { - path += ".exe"; - } else { - pathExists = false; - } - } else { - path = relativePath; - } - } - } - return { pathExists, path }; -} - -/** Read the files in a directory */ -export function readDir(dirPath: string): Promise { - return new Promise((resolve) => { - fs.readdir(dirPath, (err, list) => { - resolve(list); - }); - }); -} - -/** Reads the content of a text file */ -export function readFileText(filePath: string, encoding: BufferEncoding = "utf8"): Promise { - return new Promise((resolve, reject) => { - fs.readFile(filePath, { encoding }, (err: any, data: any) => { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); -} - -/** Writes content to a text file */ -export function writeFileText(filePath: string, content: string, encoding: BufferEncoding = "utf8"): Promise { - const folders: string[] = filePath.split(path.sep).slice(0, -1); - if (folders.length) { - // create folder path if it doesn't exist - folders.reduce((previous, folder) => { - const folderPath: string = previous + path.sep + folder; - if (!fs.existsSync(folderPath)) { - fs.mkdirSync(folderPath); - } - return folderPath; - }); - } - - return new Promise((resolve, reject) => { - fs.writeFile(filePath, content, { encoding }, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -} - -export function deleteFile(filePath: string): Promise { - return new Promise((resolve, reject) => { - if (fs.existsSync(filePath)) { - fs.unlink(filePath, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - } else { - resolve(); - } - }); -} - -export function deleteDirectory(directoryPath: string): Promise { - return new Promise((resolve, reject) => { - if (fs.existsSync(directoryPath)) { - fs.rmdir(directoryPath, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - } else { - resolve(); - } - }); -} - -export function getReadmeMessage(): string { - const readmePath: string = getExtensionFilePath("README.md"); - const readmeMessage: string = localize("refer.read.me", "Please refer to {0} for troubleshooting information. Issues can be created at {1}", readmePath, "https://github.com/Microsoft/vscode-cpptools/issues"); - return readmeMessage; -} - -/** Used for diagnostics only */ -export function logToFile(message: string): void { - const logFolder: string = getExtensionFilePath("extension.log"); - fs.writeFileSync(logFolder, `${message}${os.EOL}`, { flag: 'a' }); -} - -export function execChildProcess(process: string, workingDirectory?: string, channel?: vscode.OutputChannel): Promise { - return new Promise((resolve, reject) => { - child_process.exec(process, { cwd: workingDirectory, maxBuffer: 500 * 1024 }, (error: Error | null, stdout: string, stderr: string) => { - if (channel) { - let message: string = ""; - let err: boolean = false; - if (stdout && stdout.length > 0) { - message += stdout; - } - - if (stderr && stderr.length > 0) { - message += stderr; - err = true; - } - - if (error) { - message += error.message; - err = true; - } - - if (err) { - channel.append(message); - channel.show(); - } - } - - if (error) { - reject(error); - return; - } - - if (stderr && stderr.length > 0) { - reject(new Error(stderr)); - return; - } - - resolve(stdout); - }); - }); -} - -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) { - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - if (getNumericLoggingLevel(settings.get("loggingLevel")) >= 5) { - getOutputChannelLogger().appendLine(`$ ${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(); - - // Do not use CppSettings to avoid circular require() - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - const loggingLevel: number = (skipLogging === undefined || !skipLogging) ? getNumericLoggingLevel(settings.get("loggingLevel")) : 0; - - 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 (loggingLevel > 0) { - getOutputChannelLogger().append(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); - if (exists) { - const isExec: boolean = await isExecutable(file); - if (!isExec) { - await chmodAsync(file, '755'); - } - } else { - getOutputChannelLogger().appendLine(""); - getOutputChannelLogger().appendLine(localize("warning.file.missing", "Warning: Expected file {0} is missing.", file)); - } - } -} - -export async function chmodAsync(path: fs.PathLike, mode: fs.Mode): Promise { - return new Promise((resolve, reject) => { - fs.chmod(path, mode, (err: NodeJS.ErrnoException | null) => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -} - -export function removePotentialPII(str: string): string { - const words: string[] = str.split(" "); - let result: string = ""; - for (const word of words) { - if (!word.includes(".") && !word.includes("/") && !word.includes("\\") && !word.includes(":")) { - result += word + " "; - } else { - result += "? "; - } - } - return result; -} - -export function checkDistro(platformInfo: PlatformInformation): void { - if (platformInfo.platform !== 'win32' && platformInfo.platform !== 'linux' && platformInfo.platform !== 'darwin') { - // this should never happen because VSCode doesn't run on FreeBSD - // or SunOS (the other platforms supported by node) - getOutputChannelLogger().appendLine(localize("warning.debugging.not.tested", "Warning: Debugging has not been tested for this platform.") + " " + getReadmeMessage()); - } -} - -export async function unlinkAsync(fileName: string): Promise { - return new Promise((resolve, reject) => { - fs.unlink(fileName, err => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -} - -export async function renameAsync(oldName: string, newName: string): Promise { - return new Promise((resolve, reject) => { - fs.rename(oldName, newName, err => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -} - -export async function promptForReloadWindowDueToSettingsChange(): Promise { - await promptReloadWindow(localize("reload.workspace.for.changes", "Reload the workspace for the settings change to take effect.")); -} - -export async function promptReloadWindow(message: string): Promise { - const reload: string = localize("reload.string", "Reload"); - const value: string | undefined = await vscode.window.showInformationMessage(message, reload); - if (value === reload) { - return vscode.commands.executeCommand("workbench.action.reloadWindow"); - } -} - -export function createTempFileWithPostfix(postfix: string): Promise { - return new Promise((resolve, reject) => { - tmp.file({ postfix: postfix }, (err, path, fd, cleanupCallback) => { - if (err) { - return reject(err); - } - return resolve({ name: path, fd: fd, removeCallback: cleanupCallback } as tmp.FileResult); - }); - }); -} - -function resolveWindowsEnvironmentVariables(str: string): string { - return str.replace(/%([^%]+)%/g, (withPercents, withoutPercents) => { - const found: string | undefined = process.env[withoutPercents]; - return found || withPercents; - }); -} - -function legacyExtractArgs(argsString: string): string[] { - const result: string[] = []; - let currentArg: string = ""; - let isWithinDoubleQuote: boolean = false; - let isWithinSingleQuote: boolean = false; - for (let i: number = 0; i < argsString.length; i++) { - const c: string = argsString[i]; - if (c === '\\') { - currentArg += c; - if (++i === argsString.length) { - if (currentArg !== "") { - result.push(currentArg); - } - return result; - } - currentArg += argsString[i]; - continue; - } - if (c === '"') { - if (!isWithinSingleQuote) { - isWithinDoubleQuote = !isWithinDoubleQuote; - } - } else if (c === '\'') { - // On Windows, a single quote string is not allowed to join multiple args into a single arg - if (!isWindows) { - if (!isWithinDoubleQuote) { - isWithinSingleQuote = !isWithinSingleQuote; - } - } - } else if (c === ' ') { - if (!isWithinDoubleQuote && !isWithinSingleQuote) { - if (currentArg !== "") { - result.push(currentArg); - currentArg = ""; - } - continue; - } - } - currentArg += c; - } - if (currentArg !== "") { - result.push(currentArg); - } - return result; -} - -function extractArgs(argsString: string): string[] { - argsString = argsString.trim(); - if (os.platform() === 'win32') { - argsString = resolveWindowsEnvironmentVariables(argsString); - const result: string[] = []; - let currentArg: string = ""; - let isInQuote: boolean = false; - let wasInQuote: boolean = false; - let i: number = 0; - while (i < argsString.length) { - let c: string = argsString[i]; - if (c === '\"') { - if (!isInQuote) { - isInQuote = true; - wasInQuote = true; - ++i; - continue; - } - // Need to peek at next character. - if (++i === argsString.length) { - break; - } - c = argsString[i]; - if (c !== '\"') { - isInQuote = false; - } - // Fall through. If c was a quote character, it will be added as a literal. - } - if (c === '\\') { - let backslashCount: number = 1; - let reachedEnd: boolean = true; - while (++i !== argsString.length) { - c = argsString[i]; - if (c !== '\\') { - reachedEnd = false; - break; - } - ++backslashCount; - } - const still_escaping: boolean = (backslashCount % 2) !== 0; - if (!reachedEnd && c === '\"') { - backslashCount = Math.floor(backslashCount / 2); - } - while (backslashCount--) { - currentArg += '\\'; - } - if (reachedEnd) { - break; - } - // If not still escaping and a quote was found, it needs to be handled above. - if (!still_escaping && c === '\"') { - continue; - } - // Otherwise, fall through to handle c as a literal. - } - if (c === ' ' || c === '\t' || c === '\r' || c === '\n') { - if (!isInQuote) { - if (currentArg !== "" || wasInQuote) { - wasInQuote = false; - result.push(currentArg); - currentArg = ""; - } - i++; - continue; - } - } - currentArg += c; - i++; - } - if (currentArg !== "" || wasInQuote) { - result.push(currentArg); - } - return result; - } else { - try { - const wordexpResult: any = child_process.execFileSync(getExtensionFilePath("bin/cpptools-wordexp"), [argsString], { shell: false }); - if (wordexpResult === undefined) { - return []; - } - const jsonText: string = wordexpResult.toString(); - return jsonc.parse(jsonText, undefined, true) as any; - } catch { - return []; - } - } -} - -export function isCl(compilerPath: string): boolean { - const compilerPathLowercase: string = compilerPath.toLowerCase(); - return compilerPathLowercase === "cl" || compilerPathLowercase === "cl.exe" - || compilerPathLowercase.endsWith("\\cl.exe") || compilerPathLowercase.endsWith("/cl.exe") - || compilerPathLowercase.endsWith("\\cl") || compilerPathLowercase.endsWith("/cl"); -} - -/** CompilerPathAndArgs retains original casing of text input for compiler path and args */ -export interface CompilerPathAndArgs { - compilerPath?: string | null; - compilerName: string; - compilerArgs?: string[]; - compilerArgsFromCommandLineInPath: string[]; - allCompilerArgs: string[]; -} - -export function extractCompilerPathAndArgs(useLegacyBehavior: boolean, inputCompilerPath?: string | null, compilerArgs?: string[]): CompilerPathAndArgs { - let compilerPath: string | undefined | null = inputCompilerPath; - let compilerName: string = ""; - let compilerArgsFromCommandLineInPath: string[] = []; - if (compilerPath) { - compilerPath = compilerPath.trim(); - if (isCl(compilerPath) || checkExecutableWithoutExtensionExistsSync(compilerPath)) { - // If the path ends with cl, or if a file is found at that path, accept it without further validation. - compilerName = path.basename(compilerPath); - } else if (compilerPath.startsWith("\"") || (os.platform() !== 'win32' && compilerPath.startsWith("'"))) { - // If the string starts with a quote, treat it as a command line. - // Otherwise, a path with a leading quote would not be valid. - if (useLegacyBehavior) { - compilerArgsFromCommandLineInPath = legacyExtractArgs(compilerPath); - if (compilerArgsFromCommandLineInPath.length > 0) { - compilerPath = compilerArgsFromCommandLineInPath.shift(); - if (compilerPath) { - // Try to trim quotes from compiler path. - const tempCompilerPath: string[] | undefined = extractArgs(compilerPath); - if (tempCompilerPath && compilerPath.length > 0) { - compilerPath = tempCompilerPath[0]; - } - compilerName = path.basename(compilerPath); - } - } - } else { - compilerArgsFromCommandLineInPath = extractArgs(compilerPath); - if (compilerArgsFromCommandLineInPath.length > 0) { - compilerPath = compilerArgsFromCommandLineInPath.shift(); - if (compilerPath) { - compilerName = path.basename(compilerPath); - } - } - } - } else { - const spaceStart: number = compilerPath.lastIndexOf(" "); - if (spaceStart !== -1) { - // There is no leading quote, but a space suggests it might be a command line. - // Try processing it as a command line, and validate that by checking for the executable. - const potentialArgs: string[] = useLegacyBehavior ? legacyExtractArgs(compilerPath) : extractArgs(compilerPath); - let potentialCompilerPath: string | undefined = potentialArgs.shift(); - if (useLegacyBehavior) { - if (potentialCompilerPath) { - const tempCompilerPath: string[] | undefined = extractArgs(potentialCompilerPath); - if (tempCompilerPath && compilerPath.length > 0) { - potentialCompilerPath = tempCompilerPath[0]; - } - } - } - if (potentialCompilerPath) { - if (isCl(potentialCompilerPath) || checkExecutableWithoutExtensionExistsSync(potentialCompilerPath)) { - compilerArgsFromCommandLineInPath = potentialArgs; - compilerPath = potentialCompilerPath; - compilerName = path.basename(compilerPath); - } - } - } - } - } - let allCompilerArgs: string[] = !compilerArgs ? [] : compilerArgs; - allCompilerArgs = allCompilerArgs.concat(compilerArgsFromCommandLineInPath); - return { compilerPath, compilerName, compilerArgs, compilerArgsFromCommandLineInPath, allCompilerArgs }; -} - -export function escapeForSquiggles(s: string): string { - // Replace all \ with \\, except for \" - // Otherwise, the JSON.parse result will have the \ missing. - let newResults: string = ""; - let lastWasBackslash: boolean = false; - let lastBackslashWasEscaped: boolean = false; - for (let i: number = 0; i < s.length; i++) { - if (s[i] === '\\') { - if (lastWasBackslash) { - newResults += "\\"; - lastBackslashWasEscaped = !lastBackslashWasEscaped; - } else { - lastBackslashWasEscaped = false; - } - newResults += "\\"; - lastWasBackslash = true; - } else { - if (lastWasBackslash && (lastBackslashWasEscaped || (s[i] !== '"'))) { - newResults += "\\"; - } - lastWasBackslash = false; - lastBackslashWasEscaped = false; - newResults += s[i]; - } - } - if (lastWasBackslash) { - newResults += "\\"; - } - return newResults; -} - -export function getSenderType(sender?: any): string { - if (isString(sender)) { - return sender; - } else if (isUri(sender)) { - return 'contextMenu'; - } - return 'commandPalette'; -} - -function decodeUCS16(input: string): number[] { - const output: number[] = []; - let counter: number = 0; - const length: number = input.length; - let value: number; - let extra: number; - while (counter < length) { - value = input.charCodeAt(counter++); - // eslint-disable-next-line no-bitwise - if ((value & 0xF800) === 0xD800 && counter < length) { - // high surrogate, and there is a next character - extra = input.charCodeAt(counter++); - // eslint-disable-next-line no-bitwise - if ((extra & 0xFC00) === 0xDC00) { // low surrogate - // eslint-disable-next-line no-bitwise - output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); - } else { - output.push(value, extra); - } - } else { - output.push(value); - } - } - return output; -} - -const allowedIdentifierUnicodeRanges: number[][] = [ - [0x0030, 0x0039], // digits - [0x0041, 0x005A], // upper case letters - [0x005F, 0x005F], // underscore - [0x0061, 0x007A], // lower case letters - [0x00A8, 0x00A8], // DIARESIS - [0x00AA, 0x00AA], // FEMININE ORDINAL INDICATOR - [0x00AD, 0x00AD], // SOFT HYPHEN - [0x00AF, 0x00AF], // MACRON - [0x00B2, 0x00B5], // SUPERSCRIPT TWO - MICRO SIGN - [0x00B7, 0x00BA], // MIDDLE DOT - MASCULINE ORDINAL INDICATOR - [0x00BC, 0x00BE], // VULGAR FRACTION ONE QUARTER - VULGAR FRACTION THREE QUARTERS - [0x00C0, 0x00D6], // LATIN CAPITAL LETTER A WITH GRAVE - LATIN CAPITAL LETTER O WITH DIAERESIS - [0x00D8, 0x00F6], // LATIN CAPITAL LETTER O WITH STROKE - LATIN SMALL LETTER O WITH DIAERESIS - [0x00F8, 0x167F], // LATIN SMALL LETTER O WITH STROKE - CANADIAN SYLLABICS BLACKFOOT W - [0x1681, 0x180D], // OGHAM LETTER BEITH - MONGOLIAN FREE VARIATION SELECTOR THREE - [0x180F, 0x1FFF], // SYRIAC LETTER BETH - GREEK DASIA - [0x200B, 0x200D], // ZERO WIDTH SPACE - ZERO WIDTH JOINER - [0x202A, 0x202E], // LEFT-TO-RIGHT EMBEDDING - RIGHT-TO-LEFT OVERRIDE - [0x203F, 0x2040], // UNDERTIE - CHARACTER TIE - [0x2054, 0x2054], // INVERTED UNDERTIE - [0x2060, 0x218F], // WORD JOINER - TURNED DIGIT THREE - [0x2460, 0x24FF], // CIRCLED DIGIT ONE - NEGATIVE CIRCLED DIGIT ZERO - [0x2776, 0x2793], // DINGBAT NEGATIVE CIRCLED DIGIT ONE - DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN - [0x2C00, 0x2DFF], // GLAGOLITIC CAPITAL LETTER AZU - COMBINING CYRILLIC LETTER IOTIFIED BIG YUS - [0x2E80, 0x2FFF], // CJK RADICAL REPEAT - IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID - [0x3004, 0x3007], // JAPANESE INDUSTRIAL STANDARD SYMBOL - IDEOGRAPHIC NUMBER ZERO - [0x3021, 0x302F], // HANGZHOU NUMERAL ONE - HANGUL DOUBLE DOT TONE MARK - [0x3031, 0xD7FF], // VERTICAL KANA REPEAT MARK - HANGUL JONGSEONG PHIEUPH-THIEUTH - [0xF900, 0xFD3D], // CJK COMPATIBILITY IDEOGRAPH-F900 - ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM - [0xFD40, 0xFDCF], // ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM - ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM - [0xFDF0, 0xFE44], // ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM - PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET - [0xFE47, 0xFFFD], // PRESENTATION FORM FOR VERTICAL LEFT SQUARE BRACKET - REPLACEMENT CHARACTER - [0x10000, 0x1FFFD], // LINEAR B SYLLABLE B008 A - CHEESE WEDGE (U+1F9C0) - [0x20000, 0x2FFFD], // - [0x30000, 0x3FFFD], // - [0x40000, 0x4FFFD], // - [0x50000, 0x5FFFD], // - [0x60000, 0x6FFFD], // - [0x70000, 0x7FFFD], // - [0x80000, 0x8FFFD], // - [0x90000, 0x9FFFD], // - [0xA0000, 0xAFFFD], // - [0xB0000, 0xBFFFD], // - [0xC0000, 0xCFFFD], // - [0xD0000, 0xDFFFD], // - [0xE0000, 0xEFFFD] // LANGUAGE TAG (U+E0001) - VARIATION SELECTOR-256 (U+E01EF) -]; - -const disallowedFirstCharacterIdentifierUnicodeRanges: number[][] = [ - [0x0030, 0x0039], // digits - [0x0300, 0x036F], // COMBINING GRAVE ACCENT - COMBINING LATIN SMALL LETTER X - [0x1DC0, 0x1DFF], // COMBINING DOTTED GRAVE ACCENT - COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW - [0x20D0, 0x20FF], // COMBINING LEFT HARPOON ABOVE - COMBINING ASTERISK ABOVE - [0xFE20, 0xFE2F] // COMBINING LIGATURE LEFT HALF - COMBINING CYRILLIC TITLO RIGHT HALF -]; - -export function isValidIdentifier(candidate: string): boolean { - if (!candidate) { - return false; - } - const decoded: number[] = decodeUCS16(candidate); - if (!decoded || !decoded.length) { - return false; - } - - // Reject if first character is disallowed - for (let i: number = 0; i < disallowedFirstCharacterIdentifierUnicodeRanges.length; i++) { - const disallowedCharacters: number[] = disallowedFirstCharacterIdentifierUnicodeRanges[i]; - if (decoded[0] >= disallowedCharacters[0] && decoded[0] <= disallowedCharacters[1]) { - return false; - } - } - - for (let position: number = 0; position < decoded.length; position++) { - let found: boolean = false; - for (let i: number = 0; i < allowedIdentifierUnicodeRanges.length; i++) { - const allowedCharacters: number[] = allowedIdentifierUnicodeRanges[i]; - if (decoded[position] >= allowedCharacters[0] && decoded[position] <= allowedCharacters[1]) { - found = true; - break; - } - } - if (!found) { - return false; - } - } - return true; -} - -export function getCacheStoragePath(): string { - let defaultCachePath: string = ""; - let pathEnvironmentVariable: string | undefined; - switch (os.platform()) { - case 'win32': - defaultCachePath = "Microsoft\\vscode-cpptools\\"; - pathEnvironmentVariable = process.env.LOCALAPPDATA; - break; - case 'darwin': - defaultCachePath = "Library/Caches/vscode-cpptools/"; - pathEnvironmentVariable = os.homedir(); - break; - default: // Linux - defaultCachePath = "vscode-cpptools/"; - pathEnvironmentVariable = process.env.XDG_CACHE_HOME; - if (!pathEnvironmentVariable) { - pathEnvironmentVariable = path.join(os.homedir(), ".cache"); - } - break; - } - - return pathEnvironmentVariable ? path.join(pathEnvironmentVariable, defaultCachePath) : ""; -} - -function getUniqueWorkspaceNameHelper(workspaceFolder: vscode.WorkspaceFolder, addSubfolder: boolean): string { - const workspaceFolderName: string = workspaceFolder ? workspaceFolder.name : "untitled"; - if (!workspaceFolder || workspaceFolder.index < 1) { - return workspaceFolderName; // No duplicate names to search for. - } - for (let i: number = 0; i < workspaceFolder.index; ++i) { - if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 && vscode.workspace.workspaceFolders[i].name === workspaceFolderName) { - return addSubfolder ? path.join(workspaceFolderName, String(workspaceFolder.index)) : // Use the index as a subfolder. - workspaceFolderName + String(workspaceFolder.index); - } - } - return workspaceFolderName; // No duplicate names found. -} - -export function getUniqueWorkspaceName(workspaceFolder: vscode.WorkspaceFolder): string { - return getUniqueWorkspaceNameHelper(workspaceFolder, false); -} - -export function getUniqueWorkspaceStorageName(workspaceFolder: vscode.WorkspaceFolder): string { - return getUniqueWorkspaceNameHelper(workspaceFolder, true); -} - -export function isCodespaces(): boolean { - return !!process.env.CODESPACES; -} - -// Sequentially Resolve Promises. -export function sequentialResolve(items: T[], promiseBuilder: (item: T) => Promise): Promise { - return items.reduce(async (previousPromise, nextItem) => { - await previousPromise; - return promiseBuilder(nextItem); - }, Promise.resolve()); -} - -export function quoteArgument(argument: string): string { - // Return the argument as is if it's empty - if (!argument) { - return argument; - } - - if (os.platform() === "win32") { - // Windows-style quoting logic - if (!/[\s\t\n\v\"\\&%^]/.test(argument)) { - return argument; - } - - let quotedArgument = '"'; - let backslashCount = 0; - - for (const char of argument) { - if (char === '\\') { - backslashCount++; - } else { - if (char === '"') { - quotedArgument += '\\'.repeat(backslashCount * 2 + 1); - } else { - quotedArgument += '\\'.repeat(backslashCount); - } - quotedArgument += char; - backslashCount = 0; - } - } - - quotedArgument += '\\'.repeat(backslashCount * 2); - quotedArgument += '"'; - return quotedArgument; - } else { - // Unix-style quoting logic - if (!/[\s\t\n\v\"'\\$`|;&(){}<>*?!\[\]~^#%]/.test(argument)) { - return argument; - } - - let quotedArgument = "'"; - for (const c of argument) { - if (c === "'") { - quotedArgument += "'\\''"; - } else { - quotedArgument += c; - } - } - - quotedArgument += "'"; - return quotedArgument; - } -} - -/** - * Find PowerShell executable from PATH (for Windows only). - */ -export function findPowerShell(): string | undefined { - const dirs: string[] = (process.env.PATH || '').replace(/"+/g, '').split(';').filter(x => x); - const exts: string[] = (process.env.PATHEXT || '').split(';'); - const names: string[] = ['pwsh', 'powershell']; - for (const name of names) { - const candidates: string[] = dirs.reduce((paths, dir) => [ - ...paths, ...exts.map(ext => path.join(dir, name + ext)) - ], []); - for (const candidate of candidates) { - try { - if (fs.statSync(candidate).isFile()) { - return name; - } - } catch (e) { - return undefined; - } - } - } -} - -export function getCppToolsTargetPopulation(): TargetPopulation { - // If insiders.flag is present, consider this an insiders build. - // If release.flag is present, consider this a release build. - // Otherwise, consider this an internal build. - if (checkFileExistsSync(getExtensionFilePath("insiders.flag"))) { - return TargetPopulation.Insiders; - } else if (checkFileExistsSync(getExtensionFilePath("release.flag"))) { - return TargetPopulation.Public; - } - return TargetPopulation.Internal; -} - -export function isVsCodeInsiders(): boolean { - return extensionPath.includes(".vscode-insiders") || - extensionPath.includes(".vscode-server-insiders") || - extensionPath.includes(".vscode-exploration") || - extensionPath.includes(".vscode-server-exploration"); -} - -export function stripEscapeSequences(str: string): string { - return str - // eslint-disable-next-line no-control-regex - .replace(/\x1b\[\??[0-9]{0,3}(;[0-9]{1,3})?[a-zA-Z]/g, '') - // eslint-disable-next-line no-control-regex - .replace(/\u0008/g, '') - .replace(/\r/g, ''); -} - -export function splitLines(data: string): string[] { - return data.split(/\r?\n/g); -} - -export function escapeStringForRegex(str: string): string { - return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); -} - -export function replaceAll(str: string, searchValue: string, replaceValue: string): string { - const pattern: string = escapeStringForRegex(searchValue); - const re: RegExp = new RegExp(pattern, 'g'); - return str.replace(re, replaceValue); -} - -export interface ISshHostInfo { - hostName: string; - user?: string; - port?: number | string; -} - -export interface ISshConfigHostInfo extends ISshHostInfo { - file: string; -} - -/** user@host */ -export function getFullHostAddressNoPort(host: ISshHostInfo): string { - return host.user ? `${host.user}@${host.hostName}` : `${host.hostName}`; -} - -export function getFullHostAddress(host: ISshHostInfo): string { - const fullHostName: string = getFullHostAddressNoPort(host); - return host.port ? `${fullHostName}:${host.port}` : fullHostName; -} - -export interface ISshLocalForwardInfo { - bindAddress?: string; - port?: number | string; - host?: string; - hostPort?: number | string; - localSocket?: string; - remoteSocket?: string; -} - -export function whichAsync(name: string): Promise { - return new Promise(resolve => { - which(name, (err, resolved) => { - if (err) { - resolve(undefined); - } else { - resolve(resolved); - } - }); - }); -} - -export const documentSelector: DocumentFilter[] = [ - { scheme: 'file', language: 'c' }, - { scheme: 'file', language: 'cpp' }, - { scheme: 'file', language: 'cuda-cpp' } -]; - -export function hasMsvcEnvironment(): boolean { - const msvcEnvVars: string[] = [ - 'DevEnvDir', - 'Framework40Version', - 'FrameworkDir', - 'FrameworkVersion', - 'INCLUDE', - 'LIB', - 'LIBPATH', - 'NETFXSDKDir', - 'UCRTVersion', - 'UniversalCRTSdkDir', - 'VCIDEInstallDir', - 'VCINSTALLDIR', - 'VCToolsRedistDir', - 'VisualStudioVersion', - 'VSINSTALLDIR', - 'WindowsLibPath', - 'WindowsSdkBinPath', - 'WindowsSdkDir', - 'WindowsSDKLibVersion', - 'WindowsSDKVersion' - ]; - return msvcEnvVars.every((envVarName) => process.env[envVarName] !== undefined && process.env[envVarName] !== ''); -} - -function isIntegral(str: string): boolean { - const regex = /^-?\d+$/; - return regex.test(str); -} - -export function getNumericLoggingLevel(loggingLevel: string | undefined): number { - if (!loggingLevel) { - return 1; - } - if (isIntegral(loggingLevel)) { - return parseInt(loggingLevel, 10); - } - const lowerCaseLoggingLevel: string = loggingLevel.toLowerCase(); - switch (lowerCaseLoggingLevel) { - case "error": - return 1; - case "warning": - return 3; - case "information": - return 5; - case "debug": - return 6; - case "none": - return 0; - default: - return -1; - } -} - -export function mergeOverlappingRanges(ranges: Range[]): Range[] { - // Fix any reversed ranges. Not sure if this is needed, but ensures the input is sanitized. - const mergedRanges: Range[] = ranges.map(range => { - if (range.start.line > range.end.line || (range.start.line === range.end.line && range.start.character > range.end.character)) { - return Range.create(range.end, range.start); - } - return range; - }); - - // Merge overlapping ranges. - mergedRanges.sort((a, b) => a.start.line - b.start.line || a.start.character - b.start.character); - let lastMergedIndex = 0; // Index to keep track of the last merged range - for (let currentIndex = 0; currentIndex < ranges.length; currentIndex++) { - const currentRange = ranges[currentIndex]; // No need for a shallow copy, since we're not modifying the ranges we haven't read yet. - let nextIndex = currentIndex + 1; - while (nextIndex < ranges.length) { - const nextRange = ranges[nextIndex]; - // Check for non-overlapping ranges first - if (nextRange.start.line > currentRange.end.line || - (nextRange.start.line === currentRange.end.line && nextRange.start.character > currentRange.end.character)) { - break; - } - // Otherwise, merge the overlapping ranges - currentRange.end = { - line: Math.max(currentRange.end.line, nextRange.end.line), - character: Math.max(currentRange.end.character, nextRange.end.character) - }; - nextIndex++; - } - // Overwrite the array in-place - mergedRanges[lastMergedIndex] = currentRange; - lastMergedIndex++; - currentIndex = nextIndex - 1; // Skip the merged ranges - } - mergedRanges.length = lastMergedIndex; - return mergedRanges; -} - -// Arg quoting utility functions, copied from VS Code with minor changes. - -export interface IShellQuotingOptions { - /** - * The character used to do character escaping. - */ - escape?: string | { - escapeChar: string; - charsToEscape: string; - }; - - /** - * The character used for string quoting. - */ - strong?: string; - - /** - * The character used for weak quoting. - */ - weak?: string; -} - -export interface IQuotedString { - value: string; - quoting: 'escape' | 'strong' | 'weak'; -} - -export type CommandString = string | IQuotedString; - -export function buildShellCommandLine(originalCommand: CommandString, command: CommandString, args: CommandString[]): string { - - let shellQuoteOptions: IShellQuotingOptions; - const isWindows: boolean = os.platform() === 'win32'; - if (isWindows) { - shellQuoteOptions = { - strong: '"' - }; - } else { - shellQuoteOptions = { - escape: { - escapeChar: '\\', - charsToEscape: ' "\'' - }, - strong: '\'', - weak: '"' - }; - } - - // TODO: Support launching with PowerShell - // For PowerShell: - // { - // escape: { - // escapeChar: '`', - // charsToEscape: ' "\'()' - // }, - // strong: '\'', - // weak: '"' - // }, - - function needsQuotes(value: string): boolean { - if (value.length >= 2) { - const first = value[0] === shellQuoteOptions.strong ? shellQuoteOptions.strong : value[0] === shellQuoteOptions.weak ? shellQuoteOptions.weak : undefined; - if (first === value[value.length - 1]) { - return false; - } - } - let quote: string | undefined; - for (let i = 0; i < value.length; i++) { - // We found the end quote. - const ch = value[i]; - if (ch === quote) { - quote = undefined; - } else if (quote !== undefined) { - // skip the character. We are quoted. - continue; - } else if (ch === shellQuoteOptions.escape) { - // Skip the next character - i++; - } else if (ch === shellQuoteOptions.strong || ch === shellQuoteOptions.weak) { - quote = ch; - } else if (ch === ' ') { - return true; - } - } - return false; - } - - function quote(value: string, kind: 'escape' | 'strong' | 'weak'): [string, boolean] { - if (kind === "strong" && shellQuoteOptions.strong) { - return [shellQuoteOptions.strong + value + shellQuoteOptions.strong, true]; - } else if (kind === "weak" && shellQuoteOptions.weak) { - return [shellQuoteOptions.weak + value + shellQuoteOptions.weak, true]; - } else if (kind === "escape" && shellQuoteOptions.escape) { - if (isString(shellQuoteOptions.escape)) { - return [value.replace(/ /g, shellQuoteOptions.escape + ' '), true]; - } else { - const buffer: string[] = []; - for (const ch of shellQuoteOptions.escape.charsToEscape) { - buffer.push(`\\${ch}`); - } - const regexp: RegExp = new RegExp('[' + buffer.join(',') + ']', 'g'); - const escapeChar = shellQuoteOptions.escape.escapeChar; - return [value.replace(regexp, (match) => escapeChar + match), true]; - } - } - return [value, false]; - } - - function quoteIfNecessary(value: CommandString): [string, boolean] { - if (isString(value)) { - if (needsQuotes(value)) { - return quote(value, "strong"); - } else { - return [value, false]; - } - } else { - return quote(value.value, value.quoting); - } - } - - // If we have no args and the command is a string then use the command to stay backwards compatible with the old command line - // model. To allow variable resolving with spaces we do continue if the resolved value is different than the original one - // and the resolved one needs quoting. - if ((!args || args.length === 0) && isString(command) && (command === originalCommand as string || needsQuotes(originalCommand as string))) { - return command; - } - - const result: string[] = []; - let commandQuoted = false; - let argQuoted = false; - let value: string; - let quoted: boolean; - [value, quoted] = quoteIfNecessary(command); - result.push(value); - commandQuoted = quoted; - for (const arg of args) { - [value, quoted] = quoteIfNecessary(arg); - result.push(value); - argQuoted = argQuoted || quoted; - } - - let commandLine = result.join(' '); - // There are special rules quoted command line in cmd.exe - if (isWindows) { - commandLine = `chcp 65001>nul && ${commandLine}`; - if (commandQuoted && argQuoted) { - commandLine = '"' + commandLine + '"'; - } - commandLine = `cmd /c ${commandLine}`; - } - - return commandLine; -} - -export function findExePathInArgs(args: CommandString[]): string | undefined { - const isWindows: boolean = os.platform() === 'win32'; - let previousArg: string | undefined; - - for (const arg of args) { - const argValue = isString(arg) ? arg : arg.value; - if (previousArg === '-o') { - return argValue; - } - if (isWindows && argValue.includes('.exe')) { - if (argValue.startsWith('/Fe')) { - return argValue.substring(3); - } else if (argValue.toLowerCase().startsWith('/out:')) { - return argValue.substring(5); - } - } - - previousArg = argValue; - } - - return undefined; -} - -export function getVsCodeVersion(): number[] { - return vscode.version.split('.').map(num => parseInt(num, undefined)); -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as assert from 'assert'; +import * as child_process from 'child_process'; +import * as jsonc from 'comment-json'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as tmp from 'tmp'; +import * as vscode from 'vscode'; +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 { isWindows } from './constants'; +import { getOutputChannelLogger, showOutputChannel } from './logger'; +import { PlatformInformation } from './platform'; +import * as Telemetry from './telemetry'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); +export const failedToParseJson: string = localize("failed.to.parse.json", "Failed to parse json file, possibly due to comments or trailing commas."); + +export type Mutable = { + // eslint-disable-next-line @typescript-eslint/array-type + -readonly [P in keyof T]: T[P] extends ReadonlyArray ? Mutable[] : Mutable +}; + +export let extensionPath: string; +export let extensionContext: vscode.ExtensionContext | undefined; +export function setExtensionContext(context: vscode.ExtensionContext): void { + extensionContext = context; + extensionPath = extensionContext.extensionPath; +} + +export function setExtensionPath(path: string): void { + extensionPath = path; +} + +let cachedClangFormatPath: string | undefined; +export function getCachedClangFormatPath(): string | undefined { + return cachedClangFormatPath; +} + +export function setCachedClangFormatPath(path: string): void { + cachedClangFormatPath = path; +} + +let cachedClangTidyPath: string | undefined; +export function getCachedClangTidyPath(): string | undefined { + return cachedClangTidyPath; +} + +export function setCachedClangTidyPath(path: string): void { + cachedClangTidyPath = path; +} + +// Use this package.json to read values +export const packageJson: any = vscode.extensions.getExtension("ms-vscode.cpptools")?.packageJSON; + +// Use getRawSetting to get subcategorized settings from package.json. +// This prevents having to iterate every time we search. +let flattenedPackageJson: Map; +export function getRawSetting(key: string, breakIfMissing: boolean = false): any { + if (flattenedPackageJson === undefined) { + flattenedPackageJson = new Map(); + for (const subheading of packageJson.contributes.configuration) { + for (const setting in subheading.properties) { + flattenedPackageJson.set(setting, subheading.properties[setting]); + } + } + } + const result = flattenedPackageJson.get(key); + if (result === undefined && breakIfMissing) { + // eslint-disable-next-line no-debugger + debugger; // The setting does not exist in package.json. Check the `key`. + } + return result; +} + +export async function getRawJson(path: string | undefined): Promise { + if (!path) { + return {}; + } + const fileExists: boolean = await checkFileExists(path); + if (!fileExists) { + return {}; + } + + const fileContents: string = await readFileText(path); + let rawElement: any = {}; + try { + rawElement = jsonc.parse(fileContents, undefined, true); + } catch (error) { + throw new Error(failedToParseJson); + } + return rawElement; +} + +// This function is used to stringify the rawPackageJson. +// Do not use with util.packageJson or else the expanded +// package.json will be written back. +export function stringifyPackageJson(packageJson: string): string { + return JSON.stringify(packageJson, null, 2); +} + +export function getExtensionFilePath(extensionfile: string): string { + return path.resolve(extensionPath, extensionfile); +} + +export function getPackageJsonPath(): string { + return getExtensionFilePath("package.json"); +} + +export function getJsonPath(jsonFilaName: string, workspaceFolder?: vscode.WorkspaceFolder): string | undefined { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!editor) { + return undefined; + } + const folder: vscode.WorkspaceFolder | undefined = workspaceFolder ? workspaceFolder : vscode.workspace.getWorkspaceFolder(editor.document.uri); + if (!folder) { + return undefined; + } + return path.join(folder.uri.fsPath, ".vscode", jsonFilaName); +} + +export function getVcpkgPathDescriptorFile(): string { + if (process.platform === 'win32') { + const pathPrefix: string | undefined = process.env.LOCALAPPDATA; + if (!pathPrefix) { + throw new Error("Unable to read process.env.LOCALAPPDATA"); + } + return path.join(pathPrefix, "vcpkg/vcpkg.path.txt"); + } else { + const pathPrefix: string = os.homedir(); + return path.join(pathPrefix, ".vcpkg/vcpkg.path.txt"); + } +} + +let vcpkgRoot: string | undefined; +export function getVcpkgRoot(): string { + if (!vcpkgRoot && vcpkgRoot !== "") { + vcpkgRoot = ""; + // Check for vcpkg instance. + if (fs.existsSync(getVcpkgPathDescriptorFile())) { + let vcpkgRootTemp: string = fs.readFileSync(getVcpkgPathDescriptorFile()).toString(); + vcpkgRootTemp = vcpkgRootTemp.trim(); + if (fs.existsSync(vcpkgRootTemp)) { + vcpkgRoot = path.join(vcpkgRootTemp, "/installed").replace(/\\/g, "/"); + } + } + } + return vcpkgRoot; +} + +/** + * This is a fuzzy determination of whether a uri represents a header file. + * For the purposes of this function, a header file has no extension, or an extension that begins with the letter 'h'. + * @param document The document to check. + */ +export function isHeaderFile(uri: vscode.Uri): boolean { + const fileExt: string = path.extname(uri.fsPath); + const fileExtLower: string = fileExt.toLowerCase(); + return !fileExt || [".cuh", ".hpp", ".hh", ".hxx", ".h++", ".hp", ".h", ".ii", ".inl", ".idl", ""].some(ext => fileExtLower === ext); +} + +export function isCppFile(uri: vscode.Uri): boolean { + const fileExt: string = path.extname(uri.fsPath); + const fileExtLower: string = fileExt.toLowerCase(); + return (fileExt === ".C") || [".cu", ".cpp", ".cc", ".cxx", ".c++", ".cp", ".ino", ".ipp", ".tcc"].some(ext => fileExtLower === ext); +} + +export function isCFile(uri: vscode.Uri): boolean { + const fileExt: string = path.extname(uri.fsPath); + const fileExtLower: string = fileExt.toLowerCase(); + return (fileExt === ".C") || fileExtLower === ".c"; +} + +export function isCppOrCFile(uri: vscode.Uri | undefined): boolean { + if (!uri) { + return false; + } + return isCppFile(uri) || isCFile(uri); +} + +export function isFolderOpen(uri: vscode.Uri): boolean { + const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(uri); + return folder ? true : false; +} + +export function isEditorFileCpp(file: string): boolean { + const editor: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.find(e => e.document.uri.toString() === file); + if (!editor) { + return false; + } + return editor.document.languageId === "cpp"; +} + +// If it's C, C++, or Cuda. +export function isCpp(document: vscode.TextDocument): boolean { + return document.uri.scheme === "file" && + (document.languageId === "c" || document.languageId === "cpp" || document.languageId === "cuda-cpp"); +} + +export function isCppPropertiesJson(document: vscode.TextDocument): boolean { + return document.uri.scheme === "file" && (document.languageId === "json" || document.languageId === "jsonc") && + document.fileName.endsWith("c_cpp_properties.json"); +} +let isWorkspaceCpp: boolean = false; +export function setWorkspaceIsCpp(): void { + if (!isWorkspaceCpp) { + isWorkspaceCpp = true; + } +} + +export function getWorkspaceIsCpp(): boolean { + return isWorkspaceCpp; +} + +export function isCppOrRelated(document: vscode.TextDocument): boolean { + return isCpp(document) || isCppPropertiesJson(document) || (document.uri.scheme === "output" && document.uri.fsPath.startsWith("extension-output-ms-vscode.cpptools")) || + (isWorkspaceCpp && (document.languageId === "json" || document.languageId === "jsonc") && + ((document.fileName.endsWith("settings.json") && (document.uri.scheme === "file" || document.uri.scheme === "vscode-userdata")) || + (document.uri.scheme === "file" && document.fileName.endsWith(".code-workspace")))); +} + +let isExtensionNotReadyPromptDisplayed: boolean = false; +export const extensionNotReadyString: string = localize("extension.not.ready", 'The C/C++ extension is still installing. See the output window for more information.'); + +export function displayExtensionNotReadyPrompt(): void { + if (!isExtensionNotReadyPromptDisplayed) { + isExtensionNotReadyPromptDisplayed = true; + showOutputChannel(); + + void getOutputChannelLogger().showInformationMessage(extensionNotReadyString).then( + () => { isExtensionNotReadyPromptDisplayed = false; }, + () => { isExtensionNotReadyPromptDisplayed = false; } + ); + } +} + +// This Progress global state tracks how far users are able to get before getting blocked. +// Users start with a progress of 0 and it increases as they get further along in using the tool. +// This eliminates noise/problems due to re-installs, terminated installs that don't send errors, +// errors followed by workarounds that lead to success, etc. +const progressInstallSuccess: number = 100; +const progressExecutableStarted: number = 150; +const progressExecutableSuccess: number = 200; +const progressParseRootSuccess: number = 300; +const progressIntelliSenseNoSquiggles: number = 1000; +// Might add more IntelliSense progress measurements later. +// IntelliSense progress is separate from the install progress, because parse root can occur afterwards. + +const installProgressStr: string = "CPP." + packageJson.version + ".Progress"; +const intelliSenseProgressStr: string = "CPP." + packageJson.version + ".IntelliSenseProgress"; + +export function getProgress(): number { + return extensionContext ? extensionContext.globalState.get(installProgressStr, -1) : -1; +} + +export function getIntelliSenseProgress(): number { + return extensionContext ? extensionContext.globalState.get(intelliSenseProgressStr, -1) : -1; +} + +export function setProgress(progress: number): void { + if (extensionContext && getProgress() < progress) { + void extensionContext.globalState.update(installProgressStr, progress); + const telemetryProperties: Record = {}; + let progressName: string | undefined; + switch (progress) { + case 0: progressName = "install started"; break; + case progressInstallSuccess: progressName = "install succeeded"; break; + case progressExecutableStarted: progressName = "executable started"; break; + case progressExecutableSuccess: progressName = "executable succeeded"; break; + case progressParseRootSuccess: progressName = "parse root succeeded"; break; + } + if (progressName) { + telemetryProperties.progress = progressName; + } + Telemetry.logDebuggerEvent("progress", telemetryProperties); + } +} + +export function setIntelliSenseProgress(progress: number): void { + if (extensionContext && getIntelliSenseProgress() < progress) { + void extensionContext.globalState.update(intelliSenseProgressStr, progress); + const telemetryProperties: Record = {}; + let progressName: string | undefined; + switch (progress) { + case progressIntelliSenseNoSquiggles: progressName = "IntelliSense no squiggles"; break; + } + if (progressName) { + telemetryProperties.progress = progressName; + } + Telemetry.logDebuggerEvent("progress", telemetryProperties); + } +} + +export function getProgressInstallSuccess(): number { return progressInstallSuccess; } // Download/install was successful (i.e. not blocked by component acquisition). +export function getProgressExecutableStarted(): number { return progressExecutableStarted; } // The extension was activated and starting the executable was attempted. +export function getProgressExecutableSuccess(): number { return progressExecutableSuccess; } // Starting the exe was successful (i.e. not blocked by 32-bit or glibc < 2.18 on Linux) +export function getProgressParseRootSuccess(): number { return progressParseRootSuccess; } // Parse root was successful (i.e. not blocked by processing taking too long). +export function getProgressIntelliSenseNoSquiggles(): number { return progressIntelliSenseNoSquiggles; } // IntelliSense was successful and the user got no squiggles. + +export function isUri(input: any): input is vscode.Uri { + return input && input instanceof vscode.Uri; +} + +export function isString(input: any): input is string { + return typeof input === "string"; +} + +export function isNumber(input: any): input is number { + return typeof input === "number"; +} + +export function isBoolean(input: any): input is boolean { + return typeof input === "boolean"; +} + +export function isObject(input: any): boolean { + return input !== null && typeof input === "object" && !isArray(input); +} + +export function isArray(input: any): input is any[] { + return Array.isArray(input); +} + +export function isOptionalString(input: any): input is string | undefined { + return input === undefined || isString(input); +} + +export function isArrayOfString(input: any): input is string[] { + return isArray(input) && input.every(isString); +} + +// Validates whether the given object is a valid mapping of key and value type. +// EX: {"key": true, "key2": false} should return true for keyType = string and valueType = boolean. +export function isValidMapping(value: any, isValidKey: (key: any) => boolean, isValidValue: (value: any) => boolean): value is object { + if (isObject(value)) { + return Object.entries(value).every(([key, val]) => isValidKey(key) && isValidValue(val)); + } + return false; +} + +export function isOptionalArrayOfString(input: any): input is string[] | undefined { + return input === undefined || isArrayOfString(input); +} + +export function resolveCachePath(input: string | undefined, additionalEnvironment: Record): string { + let resolvedPath: string = ""; + if (!input || input.trim() === "") { + // If no path is set, return empty string to language service process, where it will set the default path as + // Windows: %LocalAppData%/Microsoft/vscode-cpptools/ + // Linux and Mac: ~/.vscode-cpptools/ + return resolvedPath; + } + + resolvedPath = resolveVariables(input, additionalEnvironment); + return resolvedPath; +} + +export function defaultExePath(): string { + const exePath: string = path.join('${fileDirname}', '${fileBasenameNoExtension}'); + return isWindows ? exePath + '.exe' : exePath; +} + +// Pass in 'arrayResults' if a string[] result is possible and a delimited string result is undesirable. +// The string[] result will be copied into 'arrayResults'. +export function resolveVariables(input: string | undefined, additionalEnvironment?: Record, arrayResults?: string[]): string { + if (!input) { + return ""; + } + + // jsonc parser may assign a non-string object to a string. + // TODO: https://github.com/microsoft/vscode-cpptools/issues/9414 + if (!isString(input)) { + const inputAny: any = input; + input = inputAny.toString(); + return input ?? ""; + } + + // Replace environment and configuration variables. + const regexp: () => RegExp = () => /\$\{((env|config|workspaceFolder)(\.|:))?(.*?)\}/g; + let ret: string = input; + const cycleCache = new Set(); + while (!cycleCache.has(ret)) { + cycleCache.add(ret); + ret = ret.replace(regexp(), (match: string, ignored1: string, varType: string, ignored2: string, name: string) => { + // Historically, if the variable didn't have anything before the "." or ":" + // it was assumed to be an environment variable + if (!varType) { + varType = "env"; + } + let newValue: string | undefined; + switch (varType) { + case "env": { + if (additionalEnvironment) { + const v: string | string[] | undefined = additionalEnvironment[name]; + if (isString(v)) { + newValue = v; + } else if (input === match && isArrayOfString(v)) { + if (arrayResults !== undefined) { + arrayResults.push(...v); + newValue = ""; + break; + } else { + newValue = v.join(path.delimiter); + } + } + } + if (newValue === undefined) { + newValue = process.env[name]; + } + break; + } + case "config": { + const config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(); + if (config) { + newValue = config.get(name); + } + break; + } + case "workspaceFolder": { + // Only replace ${workspaceFolder:name} variables for now. + // We may consider doing replacement of ${workspaceFolder} here later, but we would have to update the language server and also + // intercept messages with paths in them and add the ${workspaceFolder} variable back in (e.g. for light bulb suggestions) + if (name && vscode.workspace && vscode.workspace.workspaceFolders) { + const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.workspaceFolders.find(folder => folder.name.toLocaleLowerCase() === name.toLocaleLowerCase()); + if (folder) { + newValue = folder.uri.fsPath; + } + } + break; + } + default: { assert.fail("unknown varType matched"); } + } + return newValue !== undefined ? newValue : match; + }); + } + + return resolveHome(ret); +} + +export function resolveVariablesArray(variables: string[] | undefined, additionalEnvironment?: Record): string[] { + let result: string[] = []; + if (variables) { + variables.forEach(variable => { + const variablesResolved: string[] = []; + const variableResolved: string = resolveVariables(variable, additionalEnvironment, variablesResolved); + result = result.concat(variablesResolved.length === 0 ? variableResolved : variablesResolved); + }); + } + return result; +} + +// Resolve '~' at the start of the path. +export function resolveHome(filePath: string): string { + return filePath.replace(/^\~/g, os.homedir()); +} + +export function asFolder(uri: vscode.Uri): string { + let result: string = uri.toString(); + if (!result.endsWith('/')) { + result += '/'; + } + return result; +} + +/** + * get the default open command for the current platform + */ +export function getOpenCommand(): string { + if (os.platform() === 'win32') { + return 'explorer'; + } else if (os.platform() === 'darwin') { + return '/usr/bin/open'; + } else { + return '/usr/bin/xdg-open'; + } +} + +export function getDebugAdaptersPath(file: string): string { + return path.resolve(getExtensionFilePath("debugAdapters"), file); +} + +export async function fsStat(filePath: fs.PathLike): Promise { + let stats: fs.Stats | undefined; + try { + stats = await fs.promises.stat(filePath); + } catch (e) { + // File doesn't exist + return undefined; + } + return stats; +} + +export async function checkPathExists(filePath: string): Promise { + return !!await fsStat(filePath); +} + +/** Test whether a file exists */ +export async function checkFileExists(filePath: string): Promise { + const stats: fs.Stats | undefined = await fsStat(filePath); + return !!stats && stats.isFile(); +} + +/** Test whether a file exists */ +export async function checkExecutableWithoutExtensionExists(filePath: string): Promise { + if (await checkFileExists(filePath)) { + return true; + } + if (os.platform() === 'win32') { + if (filePath.length > 4) { + const possibleExtension: string = filePath.substring(filePath.length - 4).toLowerCase(); + if (possibleExtension === ".exe" || possibleExtension === ".cmd" || possibleExtension === ".bat") { + return false; + } + } + if (await checkFileExists(filePath + ".exe")) { + return true; + } + if (await checkFileExists(filePath + ".cmd")) { + return true; + } + if (await checkFileExists(filePath + ".bat")) { + return true; + } + } + return false; +} + +/** Test whether a directory exists */ +export async function checkDirectoryExists(dirPath: string): Promise { + const stats: fs.Stats | undefined = await fsStat(dirPath); + return !!stats && stats.isDirectory(); +} + +export function createDirIfNotExistsSync(filePath: string | undefined): void { + if (!filePath) { + return; + } + const dirPath: string = path.dirname(filePath); + if (!checkDirectoryExistsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } +} + +export function checkFileExistsSync(filePath: string): boolean { + try { + return fs.statSync(filePath).isFile(); + } catch (e) { + return false; + } +} + +export function checkExecutableWithoutExtensionExistsSync(filePath: string): boolean { + if (checkFileExistsSync(filePath)) { + return true; + } + if (os.platform() === 'win32') { + if (filePath.length > 4) { + const possibleExtension: string = filePath.substring(filePath.length - 4).toLowerCase(); + if (possibleExtension === ".exe" || possibleExtension === ".cmd" || possibleExtension === ".bat") { + return false; + } + } + if (checkFileExistsSync(filePath + ".exe")) { + return true; + } + if (checkFileExistsSync(filePath + ".cmd")) { + return true; + } + if (checkFileExistsSync(filePath + ".bat")) { + return true; + } + } + return false; +} + +/** Test whether a directory exists */ +export function checkDirectoryExistsSync(dirPath: string): boolean { + try { + return fs.statSync(dirPath).isDirectory(); + } catch (e) { + return false; + } +} + +/** Test whether a relative path exists */ +export function checkPathExistsSync(path: string, relativePath: string, _isWindows: boolean, isCompilerPath: boolean): { pathExists: boolean; path: string } { + let pathExists: boolean = true; + const existsWithExeAdded: (path: string) => boolean = (path: string) => isCompilerPath && _isWindows && fs.existsSync(path + ".exe"); + if (!fs.existsSync(path)) { + if (existsWithExeAdded(path)) { + path += ".exe"; + } else if (!relativePath) { + pathExists = false; + } else { + // Check again for a relative path. + relativePath = relativePath + path; + if (!fs.existsSync(relativePath)) { + if (existsWithExeAdded(path)) { + path += ".exe"; + } else { + pathExists = false; + } + } else { + path = relativePath; + } + } + } + return { pathExists, path }; +} + +/** Read the files in a directory */ +export function readDir(dirPath: string): Promise { + return new Promise((resolve) => { + fs.readdir(dirPath, (err, list) => { + resolve(list); + }); + }); +} + +/** Reads the content of a text file */ +export function readFileText(filePath: string, encoding: BufferEncoding = "utf8"): Promise { + return new Promise((resolve, reject) => { + fs.readFile(filePath, { encoding }, (err: any, data: any) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + +/** Writes content to a text file */ +export function writeFileText(filePath: string, content: string, encoding: BufferEncoding = "utf8"): Promise { + const folders: string[] = filePath.split(path.sep).slice(0, -1); + if (folders.length) { + // create folder path if it doesn't exist + folders.reduce((previous, folder) => { + const folderPath: string = previous + path.sep + folder; + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath); + } + return folderPath; + }); + } + + return new Promise((resolve, reject) => { + fs.writeFile(filePath, content, { encoding }, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +export function deleteFile(filePath: string): Promise { + return new Promise((resolve, reject) => { + if (fs.existsSync(filePath)) { + fs.unlink(filePath, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + } else { + resolve(); + } + }); +} + +export function deleteDirectory(directoryPath: string): Promise { + return new Promise((resolve, reject) => { + if (fs.existsSync(directoryPath)) { + fs.rmdir(directoryPath, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + } else { + resolve(); + } + }); +} + +export function getReadmeMessage(): string { + const readmePath: string = getExtensionFilePath("README.md"); + const readmeMessage: string = localize("refer.read.me", "Please refer to {0} for troubleshooting information. Issues can be created at {1}", readmePath, "https://github.com/Microsoft/vscode-cpptools/issues"); + return readmeMessage; +} + +/** Used for diagnostics only */ +export function logToFile(message: string): void { + const logFolder: string = getExtensionFilePath("extension.log"); + fs.writeFileSync(logFolder, `${message}${os.EOL}`, { flag: 'a' }); +} + +export function execChildProcess(process: string, workingDirectory?: string, channel?: vscode.OutputChannel): Promise { + return new Promise((resolve, reject) => { + child_process.exec(process, { cwd: workingDirectory, maxBuffer: 500 * 1024 }, (error: Error | null, stdout: string, stderr: string) => { + if (channel) { + let message: string = ""; + let err: boolean = false; + if (stdout && stdout.length > 0) { + message += stdout; + } + + if (stderr && stderr.length > 0) { + message += stderr; + err = true; + } + + if (error) { + message += error.message; + err = true; + } + + if (err) { + channel.append(message); + channel.show(); + } + } + + if (error) { + reject(error); + return; + } + + if (stderr && stderr.length > 0) { + reject(new Error(stderr)); + return; + } + + resolve(stdout); + }); + }); +} + +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().appendLine(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().append(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); + if (exists) { + const isExec: boolean = await isExecutable(file); + if (!isExec) { + await chmodAsync(file, '755'); + } + } else { + getOutputChannelLogger().appendLine(""); + getOutputChannelLogger().appendLine(localize("warning.file.missing", "Warning: Expected file {0} is missing.", file)); + } + } +} + +export async function chmodAsync(path: fs.PathLike, mode: fs.Mode): Promise { + return new Promise((resolve, reject) => { + fs.chmod(path, mode, (err: NodeJS.ErrnoException | null) => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); +} + +export function removePotentialPII(str: string): string { + const words: string[] = str.split(" "); + let result: string = ""; + for (const word of words) { + if (!word.includes(".") && !word.includes("/") && !word.includes("\\") && !word.includes(":")) { + result += word + " "; + } else { + result += "? "; + } + } + return result; +} + +export function checkDistro(platformInfo: PlatformInformation): void { + if (platformInfo.platform !== 'win32' && platformInfo.platform !== 'linux' && platformInfo.platform !== 'darwin') { + // this should never happen because VSCode doesn't run on FreeBSD + // or SunOS (the other platforms supported by node) + getOutputChannelLogger().appendLine(localize("warning.debugging.not.tested", "Warning: Debugging has not been tested for this platform.") + " " + getReadmeMessage()); + } +} + +export async function unlinkAsync(fileName: string): Promise { + return new Promise((resolve, reject) => { + fs.unlink(fileName, err => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); +} + +export async function renameAsync(oldName: string, newName: string): Promise { + return new Promise((resolve, reject) => { + fs.rename(oldName, newName, err => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); +} + +export async function promptForReloadWindowDueToSettingsChange(): Promise { + await promptReloadWindow(localize("reload.workspace.for.changes", "Reload the workspace for the settings change to take effect.")); +} + +export async function promptReloadWindow(message: string): Promise { + const reload: string = localize("reload.string", "Reload"); + const value: string | undefined = await vscode.window.showInformationMessage(message, reload); + if (value === reload) { + return vscode.commands.executeCommand("workbench.action.reloadWindow"); + } +} + +export function createTempFileWithPostfix(postfix: string): Promise { + return new Promise((resolve, reject) => { + tmp.file({ postfix: postfix }, (err, path, fd, cleanupCallback) => { + if (err) { + return reject(err); + } + return resolve({ name: path, fd: fd, removeCallback: cleanupCallback } as tmp.FileResult); + }); + }); +} + +function resolveWindowsEnvironmentVariables(str: string): string { + return str.replace(/%([^%]+)%/g, (withPercents, withoutPercents) => { + const found: string | undefined = process.env[withoutPercents]; + return found || withPercents; + }); +} + +function legacyExtractArgs(argsString: string): string[] { + const result: string[] = []; + let currentArg: string = ""; + let isWithinDoubleQuote: boolean = false; + let isWithinSingleQuote: boolean = false; + for (let i: number = 0; i < argsString.length; i++) { + const c: string = argsString[i]; + if (c === '\\') { + currentArg += c; + if (++i === argsString.length) { + if (currentArg !== "") { + result.push(currentArg); + } + return result; + } + currentArg += argsString[i]; + continue; + } + if (c === '"') { + if (!isWithinSingleQuote) { + isWithinDoubleQuote = !isWithinDoubleQuote; + } + } else if (c === '\'') { + // On Windows, a single quote string is not allowed to join multiple args into a single arg + if (!isWindows) { + if (!isWithinDoubleQuote) { + isWithinSingleQuote = !isWithinSingleQuote; + } + } + } else if (c === ' ') { + if (!isWithinDoubleQuote && !isWithinSingleQuote) { + if (currentArg !== "") { + result.push(currentArg); + currentArg = ""; + } + continue; + } + } + currentArg += c; + } + if (currentArg !== "") { + result.push(currentArg); + } + return result; +} + +function extractArgs(argsString: string): string[] { + argsString = argsString.trim(); + if (os.platform() === 'win32') { + argsString = resolveWindowsEnvironmentVariables(argsString); + const result: string[] = []; + let currentArg: string = ""; + let isInQuote: boolean = false; + let wasInQuote: boolean = false; + let i: number = 0; + while (i < argsString.length) { + let c: string = argsString[i]; + if (c === '\"') { + if (!isInQuote) { + isInQuote = true; + wasInQuote = true; + ++i; + continue; + } + // Need to peek at next character. + if (++i === argsString.length) { + break; + } + c = argsString[i]; + if (c !== '\"') { + isInQuote = false; + } + // Fall through. If c was a quote character, it will be added as a literal. + } + if (c === '\\') { + let backslashCount: number = 1; + let reachedEnd: boolean = true; + while (++i !== argsString.length) { + c = argsString[i]; + if (c !== '\\') { + reachedEnd = false; + break; + } + ++backslashCount; + } + const still_escaping: boolean = (backslashCount % 2) !== 0; + if (!reachedEnd && c === '\"') { + backslashCount = Math.floor(backslashCount / 2); + } + while (backslashCount--) { + currentArg += '\\'; + } + if (reachedEnd) { + break; + } + // If not still escaping and a quote was found, it needs to be handled above. + if (!still_escaping && c === '\"') { + continue; + } + // Otherwise, fall through to handle c as a literal. + } + if (c === ' ' || c === '\t' || c === '\r' || c === '\n') { + if (!isInQuote) { + if (currentArg !== "" || wasInQuote) { + wasInQuote = false; + result.push(currentArg); + currentArg = ""; + } + i++; + continue; + } + } + currentArg += c; + i++; + } + if (currentArg !== "" || wasInQuote) { + result.push(currentArg); + } + return result; + } else { + try { + const wordexpResult: any = child_process.execFileSync(getExtensionFilePath("bin/cpptools-wordexp"), [argsString], { shell: false }); + if (wordexpResult === undefined) { + return []; + } + const jsonText: string = wordexpResult.toString(); + return jsonc.parse(jsonText, undefined, true) as any; + } catch { + return []; + } + } +} + +export function isCl(compilerPath: string): boolean { + const compilerPathLowercase: string = compilerPath.toLowerCase(); + return compilerPathLowercase === "cl" || compilerPathLowercase === "cl.exe" + || compilerPathLowercase.endsWith("\\cl.exe") || compilerPathLowercase.endsWith("/cl.exe") + || compilerPathLowercase.endsWith("\\cl") || compilerPathLowercase.endsWith("/cl"); +} + +/** CompilerPathAndArgs retains original casing of text input for compiler path and args */ +export interface CompilerPathAndArgs { + compilerPath?: string | null; + compilerName: string; + compilerArgs?: string[]; + compilerArgsFromCommandLineInPath: string[]; + allCompilerArgs: string[]; +} + +export function extractCompilerPathAndArgs(useLegacyBehavior: boolean, inputCompilerPath?: string | null, compilerArgs?: string[]): CompilerPathAndArgs { + let compilerPath: string | undefined | null = inputCompilerPath; + let compilerName: string = ""; + let compilerArgsFromCommandLineInPath: string[] = []; + if (compilerPath) { + compilerPath = compilerPath.trim(); + if (isCl(compilerPath) || checkExecutableWithoutExtensionExistsSync(compilerPath)) { + // If the path ends with cl, or if a file is found at that path, accept it without further validation. + compilerName = path.basename(compilerPath); + } else if (compilerPath.startsWith("\"") || (os.platform() !== 'win32' && compilerPath.startsWith("'"))) { + // If the string starts with a quote, treat it as a command line. + // Otherwise, a path with a leading quote would not be valid. + if (useLegacyBehavior) { + compilerArgsFromCommandLineInPath = legacyExtractArgs(compilerPath); + if (compilerArgsFromCommandLineInPath.length > 0) { + compilerPath = compilerArgsFromCommandLineInPath.shift(); + if (compilerPath) { + // Try to trim quotes from compiler path. + const tempCompilerPath: string[] | undefined = extractArgs(compilerPath); + if (tempCompilerPath && compilerPath.length > 0) { + compilerPath = tempCompilerPath[0]; + } + compilerName = path.basename(compilerPath); + } + } + } else { + compilerArgsFromCommandLineInPath = extractArgs(compilerPath); + if (compilerArgsFromCommandLineInPath.length > 0) { + compilerPath = compilerArgsFromCommandLineInPath.shift(); + if (compilerPath) { + compilerName = path.basename(compilerPath); + } + } + } + } else { + const spaceStart: number = compilerPath.lastIndexOf(" "); + if (spaceStart !== -1) { + // There is no leading quote, but a space suggests it might be a command line. + // Try processing it as a command line, and validate that by checking for the executable. + const potentialArgs: string[] = useLegacyBehavior ? legacyExtractArgs(compilerPath) : extractArgs(compilerPath); + let potentialCompilerPath: string | undefined = potentialArgs.shift(); + if (useLegacyBehavior) { + if (potentialCompilerPath) { + const tempCompilerPath: string[] | undefined = extractArgs(potentialCompilerPath); + if (tempCompilerPath && compilerPath.length > 0) { + potentialCompilerPath = tempCompilerPath[0]; + } + } + } + if (potentialCompilerPath) { + if (isCl(potentialCompilerPath) || checkExecutableWithoutExtensionExistsSync(potentialCompilerPath)) { + compilerArgsFromCommandLineInPath = potentialArgs; + compilerPath = potentialCompilerPath; + compilerName = path.basename(compilerPath); + } + } + } + } + } + let allCompilerArgs: string[] = !compilerArgs ? [] : compilerArgs; + allCompilerArgs = allCompilerArgs.concat(compilerArgsFromCommandLineInPath); + return { compilerPath, compilerName, compilerArgs, compilerArgsFromCommandLineInPath, allCompilerArgs }; +} + +export function escapeForSquiggles(s: string): string { + // Replace all \ with \\, except for \" + // Otherwise, the JSON.parse result will have the \ missing. + let newResults: string = ""; + let lastWasBackslash: boolean = false; + let lastBackslashWasEscaped: boolean = false; + for (let i: number = 0; i < s.length; i++) { + if (s[i] === '\\') { + if (lastWasBackslash) { + newResults += "\\"; + lastBackslashWasEscaped = !lastBackslashWasEscaped; + } else { + lastBackslashWasEscaped = false; + } + newResults += "\\"; + lastWasBackslash = true; + } else { + if (lastWasBackslash && (lastBackslashWasEscaped || (s[i] !== '"'))) { + newResults += "\\"; + } + lastWasBackslash = false; + lastBackslashWasEscaped = false; + newResults += s[i]; + } + } + if (lastWasBackslash) { + newResults += "\\"; + } + return newResults; +} + +export function getSenderType(sender?: any): string { + if (isString(sender)) { + return sender; + } else if (isUri(sender)) { + return 'contextMenu'; + } + return 'commandPalette'; +} + +function decodeUCS16(input: string): number[] { + const output: number[] = []; + let counter: number = 0; + const length: number = input.length; + let value: number; + let extra: number; + while (counter < length) { + value = input.charCodeAt(counter++); + // eslint-disable-next-line no-bitwise + if ((value & 0xF800) === 0xD800 && counter < length) { + // high surrogate, and there is a next character + extra = input.charCodeAt(counter++); + // eslint-disable-next-line no-bitwise + if ((extra & 0xFC00) === 0xDC00) { // low surrogate + // eslint-disable-next-line no-bitwise + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + output.push(value, extra); + } + } else { + output.push(value); + } + } + return output; +} + +const allowedIdentifierUnicodeRanges: number[][] = [ + [0x0030, 0x0039], // digits + [0x0041, 0x005A], // upper case letters + [0x005F, 0x005F], // underscore + [0x0061, 0x007A], // lower case letters + [0x00A8, 0x00A8], // DIARESIS + [0x00AA, 0x00AA], // FEMININE ORDINAL INDICATOR + [0x00AD, 0x00AD], // SOFT HYPHEN + [0x00AF, 0x00AF], // MACRON + [0x00B2, 0x00B5], // SUPERSCRIPT TWO - MICRO SIGN + [0x00B7, 0x00BA], // MIDDLE DOT - MASCULINE ORDINAL INDICATOR + [0x00BC, 0x00BE], // VULGAR FRACTION ONE QUARTER - VULGAR FRACTION THREE QUARTERS + [0x00C0, 0x00D6], // LATIN CAPITAL LETTER A WITH GRAVE - LATIN CAPITAL LETTER O WITH DIAERESIS + [0x00D8, 0x00F6], // LATIN CAPITAL LETTER O WITH STROKE - LATIN SMALL LETTER O WITH DIAERESIS + [0x00F8, 0x167F], // LATIN SMALL LETTER O WITH STROKE - CANADIAN SYLLABICS BLACKFOOT W + [0x1681, 0x180D], // OGHAM LETTER BEITH - MONGOLIAN FREE VARIATION SELECTOR THREE + [0x180F, 0x1FFF], // SYRIAC LETTER BETH - GREEK DASIA + [0x200B, 0x200D], // ZERO WIDTH SPACE - ZERO WIDTH JOINER + [0x202A, 0x202E], // LEFT-TO-RIGHT EMBEDDING - RIGHT-TO-LEFT OVERRIDE + [0x203F, 0x2040], // UNDERTIE - CHARACTER TIE + [0x2054, 0x2054], // INVERTED UNDERTIE + [0x2060, 0x218F], // WORD JOINER - TURNED DIGIT THREE + [0x2460, 0x24FF], // CIRCLED DIGIT ONE - NEGATIVE CIRCLED DIGIT ZERO + [0x2776, 0x2793], // DINGBAT NEGATIVE CIRCLED DIGIT ONE - DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN + [0x2C00, 0x2DFF], // GLAGOLITIC CAPITAL LETTER AZU - COMBINING CYRILLIC LETTER IOTIFIED BIG YUS + [0x2E80, 0x2FFF], // CJK RADICAL REPEAT - IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID + [0x3004, 0x3007], // JAPANESE INDUSTRIAL STANDARD SYMBOL - IDEOGRAPHIC NUMBER ZERO + [0x3021, 0x302F], // HANGZHOU NUMERAL ONE - HANGUL DOUBLE DOT TONE MARK + [0x3031, 0xD7FF], // VERTICAL KANA REPEAT MARK - HANGUL JONGSEONG PHIEUPH-THIEUTH + [0xF900, 0xFD3D], // CJK COMPATIBILITY IDEOGRAPH-F900 - ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM + [0xFD40, 0xFDCF], // ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM - ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM + [0xFDF0, 0xFE44], // ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM - PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET + [0xFE47, 0xFFFD], // PRESENTATION FORM FOR VERTICAL LEFT SQUARE BRACKET - REPLACEMENT CHARACTER + [0x10000, 0x1FFFD], // LINEAR B SYLLABLE B008 A - CHEESE WEDGE (U+1F9C0) + [0x20000, 0x2FFFD], // + [0x30000, 0x3FFFD], // + [0x40000, 0x4FFFD], // + [0x50000, 0x5FFFD], // + [0x60000, 0x6FFFD], // + [0x70000, 0x7FFFD], // + [0x80000, 0x8FFFD], // + [0x90000, 0x9FFFD], // + [0xA0000, 0xAFFFD], // + [0xB0000, 0xBFFFD], // + [0xC0000, 0xCFFFD], // + [0xD0000, 0xDFFFD], // + [0xE0000, 0xEFFFD] // LANGUAGE TAG (U+E0001) - VARIATION SELECTOR-256 (U+E01EF) +]; + +const disallowedFirstCharacterIdentifierUnicodeRanges: number[][] = [ + [0x0030, 0x0039], // digits + [0x0300, 0x036F], // COMBINING GRAVE ACCENT - COMBINING LATIN SMALL LETTER X + [0x1DC0, 0x1DFF], // COMBINING DOTTED GRAVE ACCENT - COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW + [0x20D0, 0x20FF], // COMBINING LEFT HARPOON ABOVE - COMBINING ASTERISK ABOVE + [0xFE20, 0xFE2F] // COMBINING LIGATURE LEFT HALF - COMBINING CYRILLIC TITLO RIGHT HALF +]; + +export function isValidIdentifier(candidate: string): boolean { + if (!candidate) { + return false; + } + const decoded: number[] = decodeUCS16(candidate); + if (!decoded || !decoded.length) { + return false; + } + + // Reject if first character is disallowed + for (let i: number = 0; i < disallowedFirstCharacterIdentifierUnicodeRanges.length; i++) { + const disallowedCharacters: number[] = disallowedFirstCharacterIdentifierUnicodeRanges[i]; + if (decoded[0] >= disallowedCharacters[0] && decoded[0] <= disallowedCharacters[1]) { + return false; + } + } + + for (let position: number = 0; position < decoded.length; position++) { + let found: boolean = false; + for (let i: number = 0; i < allowedIdentifierUnicodeRanges.length; i++) { + const allowedCharacters: number[] = allowedIdentifierUnicodeRanges[i]; + if (decoded[position] >= allowedCharacters[0] && decoded[position] <= allowedCharacters[1]) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; +} + +export function getCacheStoragePath(): string { + let defaultCachePath: string = ""; + let pathEnvironmentVariable: string | undefined; + switch (os.platform()) { + case 'win32': + defaultCachePath = "Microsoft\\vscode-cpptools\\"; + pathEnvironmentVariable = process.env.LOCALAPPDATA; + break; + case 'darwin': + defaultCachePath = "Library/Caches/vscode-cpptools/"; + pathEnvironmentVariable = os.homedir(); + break; + default: // Linux + defaultCachePath = "vscode-cpptools/"; + pathEnvironmentVariable = process.env.XDG_CACHE_HOME; + if (!pathEnvironmentVariable) { + pathEnvironmentVariable = path.join(os.homedir(), ".cache"); + } + break; + } + + return pathEnvironmentVariable ? path.join(pathEnvironmentVariable, defaultCachePath) : ""; +} + +function getUniqueWorkspaceNameHelper(workspaceFolder: vscode.WorkspaceFolder, addSubfolder: boolean): string { + const workspaceFolderName: string = workspaceFolder ? workspaceFolder.name : "untitled"; + if (!workspaceFolder || workspaceFolder.index < 1) { + return workspaceFolderName; // No duplicate names to search for. + } + for (let i: number = 0; i < workspaceFolder.index; ++i) { + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 && vscode.workspace.workspaceFolders[i].name === workspaceFolderName) { + return addSubfolder ? path.join(workspaceFolderName, String(workspaceFolder.index)) : // Use the index as a subfolder. + workspaceFolderName + String(workspaceFolder.index); + } + } + return workspaceFolderName; // No duplicate names found. +} + +export function getUniqueWorkspaceName(workspaceFolder: vscode.WorkspaceFolder): string { + return getUniqueWorkspaceNameHelper(workspaceFolder, false); +} + +export function getUniqueWorkspaceStorageName(workspaceFolder: vscode.WorkspaceFolder): string { + return getUniqueWorkspaceNameHelper(workspaceFolder, true); +} + +export function isCodespaces(): boolean { + return !!process.env.CODESPACES; +} + +// Sequentially Resolve Promises. +export function sequentialResolve(items: T[], promiseBuilder: (item: T) => Promise): Promise { + return items.reduce(async (previousPromise, nextItem) => { + await previousPromise; + return promiseBuilder(nextItem); + }, Promise.resolve()); +} + +export function quoteArgument(argument: string): string { + // Return the argument as is if it's empty + if (!argument) { + return argument; + } + + if (os.platform() === "win32") { + // Windows-style quoting logic + if (!/[\s\t\n\v\"\\&%^]/.test(argument)) { + return argument; + } + + let quotedArgument = '"'; + let backslashCount = 0; + + for (const char of argument) { + if (char === '\\') { + backslashCount++; + } else { + if (char === '"') { + quotedArgument += '\\'.repeat(backslashCount * 2 + 1); + } else { + quotedArgument += '\\'.repeat(backslashCount); + } + quotedArgument += char; + backslashCount = 0; + } + } + + quotedArgument += '\\'.repeat(backslashCount * 2); + quotedArgument += '"'; + return quotedArgument; + } else { + // Unix-style quoting logic + if (!/[\s\t\n\v\"'\\$`|;&(){}<>*?!\[\]~^#%]/.test(argument)) { + return argument; + } + + let quotedArgument = "'"; + for (const c of argument) { + if (c === "'") { + quotedArgument += "'\\''"; + } else { + quotedArgument += c; + } + } + + quotedArgument += "'"; + return quotedArgument; + } +} + +/** + * Find PowerShell executable from PATH (for Windows only). + */ +export function findPowerShell(): string | undefined { + const dirs: string[] = (process.env.PATH || '').replace(/"+/g, '').split(';').filter(x => x); + const exts: string[] = (process.env.PATHEXT || '').split(';'); + const names: string[] = ['pwsh', 'powershell']; + for (const name of names) { + const candidates: string[] = dirs.reduce((paths, dir) => [ + ...paths, ...exts.map(ext => path.join(dir, name + ext)) + ], []); + for (const candidate of candidates) { + try { + if (fs.statSync(candidate).isFile()) { + return name; + } + } catch (e) { + return undefined; + } + } + } +} + +export function getCppToolsTargetPopulation(): TargetPopulation { + // If insiders.flag is present, consider this an insiders build. + // If release.flag is present, consider this a release build. + // Otherwise, consider this an internal build. + if (checkFileExistsSync(getExtensionFilePath("insiders.flag"))) { + return TargetPopulation.Insiders; + } else if (checkFileExistsSync(getExtensionFilePath("release.flag"))) { + return TargetPopulation.Public; + } + return TargetPopulation.Internal; +} + +export function isVsCodeInsiders(): boolean { + return extensionPath.includes(".vscode-insiders") || + extensionPath.includes(".vscode-server-insiders") || + extensionPath.includes(".vscode-exploration") || + extensionPath.includes(".vscode-server-exploration"); +} + +export function stripEscapeSequences(str: string): string { + return str + // eslint-disable-next-line no-control-regex + .replace(/\x1b\[\??[0-9]{0,3}(;[0-9]{1,3})?[a-zA-Z]/g, '') + // eslint-disable-next-line no-control-regex + .replace(/\u0008/g, '') + .replace(/\r/g, ''); +} + +export function splitLines(data: string): string[] { + return data.split(/\r?\n/g); +} + +export function escapeStringForRegex(str: string): string { + return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); +} + +export function replaceAll(str: string, searchValue: string, replaceValue: string): string { + const pattern: string = escapeStringForRegex(searchValue); + const re: RegExp = new RegExp(pattern, 'g'); + return str.replace(re, replaceValue); +} + +export interface ISshHostInfo { + hostName: string; + user?: string; + port?: number | string; +} + +export interface ISshConfigHostInfo extends ISshHostInfo { + file: string; +} + +/** user@host */ +export function getFullHostAddressNoPort(host: ISshHostInfo): string { + return host.user ? `${host.user}@${host.hostName}` : `${host.hostName}`; +} + +export function getFullHostAddress(host: ISshHostInfo): string { + const fullHostName: string = getFullHostAddressNoPort(host); + return host.port ? `${fullHostName}:${host.port}` : fullHostName; +} + +export interface ISshLocalForwardInfo { + bindAddress?: string; + port?: number | string; + host?: string; + hostPort?: number | string; + localSocket?: string; + remoteSocket?: string; +} + +export function whichAsync(name: string): Promise { + return new Promise(resolve => { + which(name, (err, resolved) => { + if (err) { + resolve(undefined); + } else { + resolve(resolved); + } + }); + }); +} + +export const documentSelector: DocumentFilter[] = [ + { scheme: 'file', language: 'c' }, + { scheme: 'file', language: 'cpp' }, + { scheme: 'file', language: 'cuda-cpp' } +]; + +export function hasMsvcEnvironment(): boolean { + const msvcEnvVars: string[] = [ + 'DevEnvDir', + 'Framework40Version', + 'FrameworkDir', + 'FrameworkVersion', + 'INCLUDE', + 'LIB', + 'LIBPATH', + 'NETFXSDKDir', + 'UCRTVersion', + 'UniversalCRTSdkDir', + 'VCIDEInstallDir', + 'VCINSTALLDIR', + 'VCToolsRedistDir', + 'VisualStudioVersion', + 'VSINSTALLDIR', + 'WindowsLibPath', + 'WindowsSdkBinPath', + 'WindowsSdkDir', + 'WindowsSDKLibVersion', + 'WindowsSDKVersion' + ]; + return msvcEnvVars.every((envVarName) => process.env[envVarName] !== undefined && process.env[envVarName] !== ''); +} + +function isIntegral(str: string): boolean { + const regex = /^-?\d+$/; + return regex.test(str); +} + +export function getLoggingLevel() { + return getNumericLoggingLevel(vscode.workspace.getConfiguration("C_Cpp", null).get("loggingLevel")); +} + +export function getNumericLoggingLevel(loggingLevel: string | undefined): number { + if (!loggingLevel) { + return 1; + } + if (isIntegral(loggingLevel)) { + return parseInt(loggingLevel, 10); + } + const lowerCaseLoggingLevel: string = loggingLevel.toLowerCase(); + switch (lowerCaseLoggingLevel) { + case "error": + return 1; + case "warning": + return 3; + case "information": + return 5; + case "debug": + return 6; + case "none": + return 0; + default: + return -1; + } +} + +export function mergeOverlappingRanges(ranges: Range[]): Range[] { + // Fix any reversed ranges. Not sure if this is needed, but ensures the input is sanitized. + const mergedRanges: Range[] = ranges.map(range => { + if (range.start.line > range.end.line || (range.start.line === range.end.line && range.start.character > range.end.character)) { + return Range.create(range.end, range.start); + } + return range; + }); + + // Merge overlapping ranges. + mergedRanges.sort((a, b) => a.start.line - b.start.line || a.start.character - b.start.character); + let lastMergedIndex = 0; // Index to keep track of the last merged range + for (let currentIndex = 0; currentIndex < ranges.length; currentIndex++) { + const currentRange = ranges[currentIndex]; // No need for a shallow copy, since we're not modifying the ranges we haven't read yet. + let nextIndex = currentIndex + 1; + while (nextIndex < ranges.length) { + const nextRange = ranges[nextIndex]; + // Check for non-overlapping ranges first + if (nextRange.start.line > currentRange.end.line || + (nextRange.start.line === currentRange.end.line && nextRange.start.character > currentRange.end.character)) { + break; + } + // Otherwise, merge the overlapping ranges + currentRange.end = { + line: Math.max(currentRange.end.line, nextRange.end.line), + character: Math.max(currentRange.end.character, nextRange.end.character) + }; + nextIndex++; + } + // Overwrite the array in-place + mergedRanges[lastMergedIndex] = currentRange; + lastMergedIndex++; + currentIndex = nextIndex - 1; // Skip the merged ranges + } + mergedRanges.length = lastMergedIndex; + return mergedRanges; +} + +// Arg quoting utility functions, copied from VS Code with minor changes. + +export interface IShellQuotingOptions { + /** + * The character used to do character escaping. + */ + escape?: string | { + escapeChar: string; + charsToEscape: string; + }; + + /** + * The character used for string quoting. + */ + strong?: string; + + /** + * The character used for weak quoting. + */ + weak?: string; +} + +export interface IQuotedString { + value: string; + quoting: 'escape' | 'strong' | 'weak'; +} + +export type CommandString = string | IQuotedString; + +export function buildShellCommandLine(originalCommand: CommandString, command: CommandString, args: CommandString[]): string { + + let shellQuoteOptions: IShellQuotingOptions; + const isWindows: boolean = os.platform() === 'win32'; + if (isWindows) { + shellQuoteOptions = { + strong: '"' + }; + } else { + shellQuoteOptions = { + escape: { + escapeChar: '\\', + charsToEscape: ' "\'' + }, + strong: '\'', + weak: '"' + }; + } + + // TODO: Support launching with PowerShell + // For PowerShell: + // { + // escape: { + // escapeChar: '`', + // charsToEscape: ' "\'()' + // }, + // strong: '\'', + // weak: '"' + // }, + + function needsQuotes(value: string): boolean { + if (value.length >= 2) { + const first = value[0] === shellQuoteOptions.strong ? shellQuoteOptions.strong : value[0] === shellQuoteOptions.weak ? shellQuoteOptions.weak : undefined; + if (first === value[value.length - 1]) { + return false; + } + } + let quote: string | undefined; + for (let i = 0; i < value.length; i++) { + // We found the end quote. + const ch = value[i]; + if (ch === quote) { + quote = undefined; + } else if (quote !== undefined) { + // skip the character. We are quoted. + continue; + } else if (ch === shellQuoteOptions.escape) { + // Skip the next character + i++; + } else if (ch === shellQuoteOptions.strong || ch === shellQuoteOptions.weak) { + quote = ch; + } else if (ch === ' ') { + return true; + } + } + return false; + } + + function quote(value: string, kind: 'escape' | 'strong' | 'weak'): [string, boolean] { + if (kind === "strong" && shellQuoteOptions.strong) { + return [shellQuoteOptions.strong + value + shellQuoteOptions.strong, true]; + } else if (kind === "weak" && shellQuoteOptions.weak) { + return [shellQuoteOptions.weak + value + shellQuoteOptions.weak, true]; + } else if (kind === "escape" && shellQuoteOptions.escape) { + if (isString(shellQuoteOptions.escape)) { + return [value.replace(/ /g, shellQuoteOptions.escape + ' '), true]; + } else { + const buffer: string[] = []; + for (const ch of shellQuoteOptions.escape.charsToEscape) { + buffer.push(`\\${ch}`); + } + const regexp: RegExp = new RegExp('[' + buffer.join(',') + ']', 'g'); + const escapeChar = shellQuoteOptions.escape.escapeChar; + return [value.replace(regexp, (match) => escapeChar + match), true]; + } + } + return [value, false]; + } + + function quoteIfNecessary(value: CommandString): [string, boolean] { + if (isString(value)) { + if (needsQuotes(value)) { + return quote(value, "strong"); + } else { + return [value, false]; + } + } else { + return quote(value.value, value.quoting); + } + } + + // If we have no args and the command is a string then use the command to stay backwards compatible with the old command line + // model. To allow variable resolving with spaces we do continue if the resolved value is different than the original one + // and the resolved one needs quoting. + if ((!args || args.length === 0) && isString(command) && (command === originalCommand as string || needsQuotes(originalCommand as string))) { + return command; + } + + const result: string[] = []; + let commandQuoted = false; + let argQuoted = false; + let value: string; + let quoted: boolean; + [value, quoted] = quoteIfNecessary(command); + result.push(value); + commandQuoted = quoted; + for (const arg of args) { + [value, quoted] = quoteIfNecessary(arg); + result.push(value); + argQuoted = argQuoted || quoted; + } + + let commandLine = result.join(' '); + // There are special rules quoted command line in cmd.exe + if (isWindows) { + commandLine = `chcp 65001>nul && ${commandLine}`; + if (commandQuoted && argQuoted) { + commandLine = '"' + commandLine + '"'; + } + commandLine = `cmd /c ${commandLine}`; + } + + return commandLine; +} + +export function findExePathInArgs(args: CommandString[]): string | undefined { + const isWindows: boolean = os.platform() === 'win32'; + let previousArg: string | undefined; + + for (const arg of args) { + const argValue = isString(arg) ? arg : arg.value; + if (previousArg === '-o') { + return argValue; + } + if (isWindows && argValue.includes('.exe')) { + if (argValue.startsWith('/Fe')) { + return argValue.substring(3); + } else if (argValue.toLowerCase().startsWith('/out:')) { + return argValue.substring(5); + } + } + + previousArg = argValue; + } + + return undefined; +} + +export function getVsCodeVersion(): number[] { + return vscode.version.split('.').map(num => parseInt(num, undefined)); +} diff --git a/Extension/src/cppTools.ts b/Extension/src/cppTools.ts index ca6bb98613..ccc6036564 100644 --- a/Extension/src/cppTools.ts +++ b/Extension/src/cppTools.ts @@ -1,135 +1,130 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import { CustomConfigurationProvider, Version } from 'vscode-cpptools'; -import { CppToolsTestApi, CppToolsTestHook } from 'vscode-cpptools/out/testApi'; -import * as nls from 'vscode-nls'; -import { CustomConfigurationProvider1, CustomConfigurationProviderCollection, getCustomConfigProviders } from './LanguageServer/customProviders'; -import * as LanguageServer from './LanguageServer/extension'; -import { CppSettings } from './LanguageServer/settings'; -import { getNumericLoggingLevel } from './common'; -import { getOutputChannel } from './logger'; -import * as test from './testHook'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -export class CppTools implements CppToolsTestApi { - private version: Version; - private providers: CustomConfigurationProvider1[] = []; - private failedRegistrations: CustomConfigurationProvider[] = []; - private timers = new Map(); - - constructor(version: Version) { - if (version > Version.latest) { - console.warn(`version ${version} is not supported by this version of cpptools`); - console.warn(` using ${Version.latest} instead`); - version = Version.latest; - } - this.version = version; - } - - private addNotifyReadyTimer(provider: CustomConfigurationProvider1): void { - if (this.version >= Version.v2) { - const timeout: number = 30; - const timer: NodeJS.Timeout = global.setTimeout(() => { - console.warn(`registered provider ${provider.extensionId} did not call 'notifyReady' within ${timeout} seconds`); - }, timeout * 1000); - this.timers.set(provider.extensionId, timer); - } - } - - private removeNotifyReadyTimer(provider: CustomConfigurationProvider1): void { - if (this.version >= Version.v2) { - const timer: NodeJS.Timeout | undefined = this.timers.get(provider.extensionId); - if (timer) { - this.timers.delete(provider.extensionId); - clearTimeout(timer); - } - } - } - - public getVersion(): Version { - return this.version; - } - - public registerCustomConfigurationProvider(provider: CustomConfigurationProvider): void { - const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); - if (providers.add(provider, this.version)) { - const added: CustomConfigurationProvider1 | undefined = providers.get(provider); - if (added) { - const settings: CppSettings = new CppSettings(); - if (getNumericLoggingLevel(settings.loggingLevel) >= 5) { - getOutputChannel().appendLine(localize("provider.registered", "Custom configuration provider '{0}' registered", added.name)); - } - this.providers.push(added); - LanguageServer.getClients().forEach(client => void client.onRegisterCustomConfigurationProvider(added)); - this.addNotifyReadyTimer(added); - } - } else { - this.failedRegistrations.push(provider); - } - } - - public notifyReady(provider: CustomConfigurationProvider): void { - const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); - const p: CustomConfigurationProvider1 | undefined = providers.get(provider); - - if (p) { - this.removeNotifyReadyTimer(p); - p.isReady = true; - LanguageServer.getClients().forEach(client => { - void client.updateCustomBrowseConfiguration(p); - void client.updateCustomConfigurations(p); - }); - } else if (this.failedRegistrations.find(p => p === provider)) { - console.warn("provider not successfully registered; 'notifyReady' ignored"); - } else { - console.warn("provider should be registered before signaling it's ready to provide configurations"); - } - } - - public didChangeCustomConfiguration(provider: CustomConfigurationProvider): void { - const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); - const p: CustomConfigurationProvider1 | undefined = providers.get(provider); - - if (p) { - if (!p.isReady) { - console.warn("didChangeCustomConfiguration was invoked before notifyReady"); - } - LanguageServer.getClients().forEach(client => void client.updateCustomConfigurations(p)); - } else if (this.failedRegistrations.find(p => p === provider)) { - console.warn("provider not successfully registered, 'didChangeCustomConfiguration' ignored"); - } else { - console.warn("provider should be registered before sending config change messages"); - } - } - - public didChangeCustomBrowseConfiguration(provider: CustomConfigurationProvider): void { - const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); - const p: CustomConfigurationProvider1 | undefined = providers.get(provider); - - if (p) { - LanguageServer.getClients().forEach(client => void client.updateCustomBrowseConfiguration(p)); - } else if (this.failedRegistrations.find(p => p === provider)) { - console.warn("provider not successfully registered, 'didChangeCustomBrowseConfiguration' ignored"); - } else { - console.warn("provider should be registered before sending config change messages"); - } - } - - public dispose(): void { - this.providers.forEach(provider => { - getCustomConfigProviders().remove(provider); - provider.dispose(); - }); - this.providers = []; - } - - public getTestHook(): CppToolsTestHook { - return test.getTestHook(); - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import { CustomConfigurationProvider, Version } from 'vscode-cpptools'; +import { CppToolsTestApi, CppToolsTestHook } from 'vscode-cpptools/out/testApi'; +import * as nls from 'vscode-nls'; +import { CustomConfigurationProvider1, CustomConfigurationProviderCollection, getCustomConfigProviders } from './LanguageServer/customProviders'; +import * as LanguageServer from './LanguageServer/extension'; +import { getOutputChannelLogger } from './logger'; +import * as test from './testHook'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export class CppTools implements CppToolsTestApi { + private version: Version; + private providers: CustomConfigurationProvider1[] = []; + private failedRegistrations: CustomConfigurationProvider[] = []; + private timers = new Map(); + + constructor(version: Version) { + if (version > Version.latest) { + console.warn(`version ${version} is not supported by this version of cpptools`); + console.warn(` using ${Version.latest} instead`); + version = Version.latest; + } + this.version = version; + } + + private addNotifyReadyTimer(provider: CustomConfigurationProvider1): void { + if (this.version >= Version.v2) { + const timeout: number = 30; + const timer: NodeJS.Timeout = global.setTimeout(() => { + console.warn(`registered provider ${provider.extensionId} did not call 'notifyReady' within ${timeout} seconds`); + }, timeout * 1000); + this.timers.set(provider.extensionId, timer); + } + } + + private removeNotifyReadyTimer(provider: CustomConfigurationProvider1): void { + if (this.version >= Version.v2) { + const timer: NodeJS.Timeout | undefined = this.timers.get(provider.extensionId); + if (timer) { + this.timers.delete(provider.extensionId); + clearTimeout(timer); + } + } + } + + public getVersion(): Version { + return this.version; + } + + public registerCustomConfigurationProvider(provider: CustomConfigurationProvider): void { + const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); + if (providers.add(provider, this.version)) { + const added: CustomConfigurationProvider1 | undefined = providers.get(provider); + if (added) { + getOutputChannelLogger().appendLine(5, localize("provider.registered", "Custom configuration provider '{0}' registered", added.name)); + this.providers.push(added); + LanguageServer.getClients().forEach(client => void client.onRegisterCustomConfigurationProvider(added)); + this.addNotifyReadyTimer(added); + } + } else { + this.failedRegistrations.push(provider); + } + } + + public notifyReady(provider: CustomConfigurationProvider): void { + const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); + const p: CustomConfigurationProvider1 | undefined = providers.get(provider); + + if (p) { + this.removeNotifyReadyTimer(p); + p.isReady = true; + LanguageServer.getClients().forEach(client => { + void client.updateCustomBrowseConfiguration(p); + void client.updateCustomConfigurations(p); + }); + } else if (this.failedRegistrations.find(p => p === provider)) { + console.warn("provider not successfully registered; 'notifyReady' ignored"); + } else { + console.warn("provider should be registered before signaling it's ready to provide configurations"); + } + } + + public didChangeCustomConfiguration(provider: CustomConfigurationProvider): void { + const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); + const p: CustomConfigurationProvider1 | undefined = providers.get(provider); + + if (p) { + if (!p.isReady) { + console.warn("didChangeCustomConfiguration was invoked before notifyReady"); + } + LanguageServer.getClients().forEach(client => void client.updateCustomConfigurations(p)); + } else if (this.failedRegistrations.find(p => p === provider)) { + console.warn("provider not successfully registered, 'didChangeCustomConfiguration' ignored"); + } else { + console.warn("provider should be registered before sending config change messages"); + } + } + + public didChangeCustomBrowseConfiguration(provider: CustomConfigurationProvider): void { + const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); + const p: CustomConfigurationProvider1 | undefined = providers.get(provider); + + if (p) { + LanguageServer.getClients().forEach(client => void client.updateCustomBrowseConfiguration(p)); + } else if (this.failedRegistrations.find(p => p === provider)) { + console.warn("provider not successfully registered, 'didChangeCustomBrowseConfiguration' ignored"); + } else { + console.warn("provider should be registered before sending config change messages"); + } + } + + public dispose(): void { + this.providers.forEach(provider => { + getCustomConfigProviders().remove(provider); + provider.dispose(); + }); + this.providers = []; + } + + public getTestHook(): CppToolsTestHook { + return test.getTestHook(); + } +} diff --git a/Extension/src/instrumentation.ts b/Extension/src/instrumentation.ts new file mode 100644 index 0000000000..3de1409b03 --- /dev/null +++ b/Extension/src/instrumentation.ts @@ -0,0 +1,69 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +/* eslint @typescript-eslint/no-var-requires: "off" */ + +export interface PerfMessage | undefined> { + /** this is the 'name' of the event */ + name: string; + + /** this indicates the context or origin of the message */ + context: Record>; + + /** if the message contains complex data, it should be in here */ + data?: TInput; + + /** if the message is just a text message, this is the contents of the message */ + text?: string; + + /** the message can have a numeric value that indicates the 'level' or 'severity' etc */ + level?: number; +} + +const services = { + instrument: >(instance: T, _options?: { ignore?: string[]; name?: string }): T => instance, + message: (_message: PerfMessage) => { }, + init: (_vscode: any) => { }, + launchSettings: undefined as Record | undefined +}; + +export function instrument>(instance: T, options?: { ignore?: string[]; name?: string }): T { + return services.instrument(instance, options); +} + +/** sends a perf message to the monitor */ +export function sendInstrumentation(message: PerfMessage): void { + services.message(message); +} + +/** verifies that the instrumentation is loaded into the global namespace */ +export function isInstrumentationEnabled(): boolean { + return !!(global as any).instrumentation; +} + +// if the instrumentation code is not globally loaded yet, then load it now +if (!isInstrumentationEnabled()) { + // pull the launch settings from the environment if the variable has been set. + if (services.launchSettings === undefined) { + services.launchSettings = process.env.PERFECTO_LAUNCH_SETTINGS ? JSON.parse(process.env.PERFECTO_LAUNCH_SETTINGS) as Record : { tests: [], collector: undefined }; + } + + // this loads the bootstrap module (global-instrumentation-support) which adds some global functions + if (services.launchSettings?.bootstrapModule) { + void require(services.launchSettings.bootstrapModule); + } +} + +// If the instrumentation object was loaded then we can set the services from the global object +if ((global as any).instrumentation) { + // instrumentation services provided by the tool + services.instrument = (global as any).instrumentation.instrument; + services.message = (global as any).instrumentation.message; + services.init = (global as any).instrumentation.init; + + services.init(require('vscode')); +} + diff --git a/Extension/src/logger.ts b/Extension/src/logger.ts index 4dba0e1d3e..e85ef10538 100644 --- a/Extension/src/logger.ts +++ b/Extension/src/logger.ts @@ -1,190 +1,194 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import * as os from 'os'; -import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; -import { getNumericLoggingLevel } from './common'; -import { CppSourceStr } from './LanguageServer/extension'; -import { getLocalizedString, LocalizeStringParams } from './LanguageServer/localization'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -// This is used for testing purposes -let Subscriber: (message: string) => void; -export function subscribeToAllLoggers(subscriber: (message: string) => void): void { - Subscriber = subscriber; -} - -export class Logger { - private writer: (message: string) => void; - - constructor(writer: (message: string) => void) { - this.writer = writer; - } - - public append(message: string): void { - this.writer(message); - if (Subscriber) { - Subscriber(message); - } - } - - public appendLine(message: string): void { - this.writer(message + os.EOL); - if (Subscriber) { - Subscriber(message + os.EOL); - } - } - - // We should not await on this function. - public showInformationMessage(message: string, items?: string[]): Thenable { - this.appendLine(message); - - if (!items) { - return vscode.window.showInformationMessage(message); - } - return vscode.window.showInformationMessage(message, ...items); - } - - // We should not await on this function. - public showWarningMessage(message: string, items?: string[]): Thenable { - this.appendLine(message); - - if (!items) { - return vscode.window.showWarningMessage(message); - } - return vscode.window.showWarningMessage(message, ...items); - } - - // We should not await on this function. - public showErrorMessage(message: string, items?: string[]): Thenable { - this.appendLine(message); - - if (!items) { - return vscode.window.showErrorMessage(message); - } - return vscode.window.showErrorMessage(message, ...items); - } -} - -export let outputChannel: vscode.OutputChannel | undefined; -export let diagnosticsChannel: vscode.OutputChannel | undefined; -export let crashCallStacksChannel: vscode.OutputChannel | undefined; -export let debugChannel: vscode.OutputChannel | undefined; -export let warningChannel: vscode.OutputChannel | undefined; -export let sshChannel: vscode.OutputChannel | undefined; - -export function getOutputChannel(): vscode.OutputChannel { - if (!outputChannel) { - outputChannel = vscode.window.createOutputChannel(CppSourceStr); - // Do not use CppSettings to avoid circular require() - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - const loggingLevel: string | undefined = settings.get("loggingLevel"); - if (getNumericLoggingLevel(loggingLevel) > 1) { - outputChannel.appendLine(`loggingLevel: ${loggingLevel}`); - } - } - return outputChannel; -} - -export function getDiagnosticsChannel(): vscode.OutputChannel { - if (!diagnosticsChannel) { - diagnosticsChannel = vscode.window.createOutputChannel(localize("c.cpp.diagnostics", "C/C++ Diagnostics")); - } - return diagnosticsChannel; -} - -export function getCrashCallStacksChannel(): vscode.OutputChannel { - if (!crashCallStacksChannel) { - crashCallStacksChannel = vscode.window.createOutputChannel(localize("c.cpp.crash.call.stacks.title", "C/C++ Crash Call Stacks")); - crashCallStacksChannel.appendLine(localize({ key: "c.cpp.crash.call.stacks.description", comment: ["{0} is a URL."] }, - "A C/C++ extension process has crashed. The crashing process name, date/time, signal, and call stack are below -- it would be helpful to include that in a bug report at {0}.", - "https://github.com/Microsoft/vscode-cpptools/issues")); - } - return crashCallStacksChannel; -} - -export function getSshChannel(): vscode.OutputChannel { - if (!sshChannel) { - sshChannel = vscode.window.createOutputChannel(localize("c.cpp.ssh.channel", "{0}: SSH", "Cpptools")); - } - return sshChannel; -} - -export function showOutputChannel(): void { - getOutputChannel().show(); -} - -let outputChannelLogger: Logger | undefined; - -export function getOutputChannelLogger(): Logger { - if (!outputChannelLogger) { - outputChannelLogger = new Logger(message => getOutputChannel().append(message)); - } - return outputChannelLogger; -} - -export function log(output: string): void { - if (!outputChannel) { - outputChannel = getOutputChannel(); - } - outputChannel.appendLine(`${output}`); -} - -export interface DebugProtocolParams { - jsonrpc: string; - method: string; - params?: any; -} - -export function logDebugProtocol(output: DebugProtocolParams): void { - if (!debugChannel) { - debugChannel = vscode.window.createOutputChannel(`${localize("c.cpp.debug.protocol", "C/C++ Debug Protocol")}`); - } - debugChannel.appendLine(""); - debugChannel.appendLine("************************************************************************************************************************"); - debugChannel.append(`${output}`); -} - -export interface ShowWarningParams { - localizeStringParams: LocalizeStringParams; -} - -export function showWarning(params: ShowWarningParams): void { - const message: string = getLocalizedString(params.localizeStringParams); - let showChannel: boolean = false; - if (!warningChannel) { - warningChannel = vscode.window.createOutputChannel(`${localize("c.cpp.warnings", "C/C++ Configuration Warnings")}`); - showChannel = true; - } - // Append before showing the channel, to avoid a delay. - warningChannel.appendLine(`[${new Date().toLocaleString()}] ${message}`); - if (showChannel) { - warningChannel.show(true); - } -} - -export function logLocalized(params: LocalizeStringParams): void { - const output: string = getLocalizedString(params); - log(output); -} - -export function disposeOutputChannels(): void { - if (outputChannel) { - outputChannel.dispose(); - } - if (diagnosticsChannel) { - diagnosticsChannel.dispose(); - } - if (debugChannel) { - debugChannel.dispose(); - } - if (warningChannel) { - warningChannel.dispose(); - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import * as os from 'os'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { getLoggingLevel } from './common'; +import { sendInstrumentation } from './instrumentation'; +import { CppSourceStr } from './LanguageServer/extension'; +import { getLocalizedString, LocalizeStringParams } from './LanguageServer/localization'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +// This is used for testing purposes +let Subscriber: (message: string) => void; +export function subscribeToAllLoggers(subscriber: (message: string) => void): void { + Subscriber = subscriber; +} + +export class Logger { + private writer: (message: string) => void; + + constructor(writer: (message: string) => void) { + this.writer = writer; + } + + public append(level: number, message: string): void; + public append(message: string): void; + public append(levelOrMessage: string | number, message?: string): void { + message = message || levelOrMessage as string; + const hasLevel = typeof levelOrMessage === 'number'; + + if (!hasLevel || getLoggingLevel() >= levelOrMessage) { + this.writer(message); + if (Subscriber) { + Subscriber(message); + } + } + sendInstrumentation({ name: 'log', text: message, context: { channel: 'log', source: 'extension' }, level: hasLevel ? levelOrMessage : undefined }); + } + + public appendLine(level: number, message: string): void; + public appendLine(message: string): void; + public appendLine(levelOrMessage: string | number, message?: string): void { + this.append(levelOrMessage as number, message + os.EOL); + } + + // We should not await on this function. + public showInformationMessage(message: string, items?: string[]): Thenable { + this.appendLine(message); + + if (!items) { + return vscode.window.showInformationMessage(message); + } + return vscode.window.showInformationMessage(message, ...items); + } + + // We should not await on this function. + public showWarningMessage(message: string, items?: string[]): Thenable { + this.appendLine(message); + + if (!items) { + return vscode.window.showWarningMessage(message); + } + return vscode.window.showWarningMessage(message, ...items); + } + + // We should not await on this function. + public showErrorMessage(message: string, items?: string[]): Thenable { + this.appendLine(message); + + if (!items) { + return vscode.window.showErrorMessage(message); + } + return vscode.window.showErrorMessage(message, ...items); + } +} + +export let outputChannel: vscode.OutputChannel | undefined; +export let diagnosticsChannel: vscode.OutputChannel | undefined; +export let crashCallStacksChannel: vscode.OutputChannel | undefined; +export let debugChannel: vscode.OutputChannel | undefined; +export let warningChannel: vscode.OutputChannel | undefined; +export let sshChannel: vscode.OutputChannel | undefined; + +export function getOutputChannel(): vscode.OutputChannel { + if (!outputChannel) { + outputChannel = vscode.window.createOutputChannel(CppSourceStr); + // Do not use CppSettings to avoid circular require() + const loggingLevel = getLoggingLevel(); + if (loggingLevel > 1) { + outputChannel.appendLine(`loggingLevel: ${loggingLevel}`); + } + } + return outputChannel; +} + +export function getDiagnosticsChannel(): vscode.OutputChannel { + if (!diagnosticsChannel) { + diagnosticsChannel = vscode.window.createOutputChannel(localize("c.cpp.diagnostics", "C/C++ Diagnostics")); + } + return diagnosticsChannel; +} + +export function getCrashCallStacksChannel(): vscode.OutputChannel { + if (!crashCallStacksChannel) { + crashCallStacksChannel = vscode.window.createOutputChannel(localize("c.cpp.crash.call.stacks.title", "C/C++ Crash Call Stacks")); + crashCallStacksChannel.appendLine(localize({ key: "c.cpp.crash.call.stacks.description", comment: ["{0} is a URL."] }, + "A C/C++ extension process has crashed. The crashing process name, date/time, signal, and call stack are below -- it would be helpful to include that in a bug report at {0}.", + "https://github.com/Microsoft/vscode-cpptools/issues")); + } + return crashCallStacksChannel; +} + +export function getSshChannel(): vscode.OutputChannel { + if (!sshChannel) { + sshChannel = vscode.window.createOutputChannel(localize("c.cpp.ssh.channel", "{0}: SSH", "Cpptools")); + } + return sshChannel; +} + +export function showOutputChannel(): void { + getOutputChannel().show(); +} + +let outputChannelLogger: Logger | undefined; + +export function getOutputChannelLogger(): Logger { + if (!outputChannelLogger) { + outputChannelLogger = new Logger(message => getOutputChannel().append(message)); + } + return outputChannelLogger; +} + +export function log(output: string): void { + getOutputChannel().appendLine(`${output}`); +} + +export interface DebugProtocolParams { + jsonrpc: string; + method: string; + params?: any; +} + +export function logDebugProtocol(output: DebugProtocolParams): void { + if (!debugChannel) { + debugChannel = vscode.window.createOutputChannel(`${localize("c.cpp.debug.protocol", "C/C++ Debug Protocol")}`); + } + debugChannel.appendLine(""); + debugChannel.appendLine("************************************************************************************************************************"); + debugChannel.append(`${output}`); +} + +export interface ShowWarningParams { + localizeStringParams: LocalizeStringParams; +} + +export function showWarning(params: ShowWarningParams): void { + const message: string = getLocalizedString(params.localizeStringParams); + let showChannel: boolean = false; + if (!warningChannel) { + warningChannel = vscode.window.createOutputChannel(`${localize("c.cpp.warnings", "C/C++ Configuration Warnings")}`); + showChannel = true; + } + // Append before showing the channel, to avoid a delay. + warningChannel.appendLine(`[${new Date().toLocaleString()}] ${message}`); + if (showChannel) { + warningChannel.show(true); + } +} + +export function logLocalized(params: LocalizeStringParams): void { + const output: string = getLocalizedString(params); + log(output); +} + +export function disposeOutputChannels(): void { + if (outputChannel) { + outputChannel.dispose(); + } + if (diagnosticsChannel) { + diagnosticsChannel.dispose(); + } + if (debugChannel) { + debugChannel.dispose(); + } + if (warningChannel) { + warningChannel.dispose(); + } +} diff --git a/Extension/src/main.ts b/Extension/src/main.ts index e15389ddac..66cc486118 100644 --- a/Extension/src/main.ts +++ b/Extension/src/main.ts @@ -1,318 +1,328 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import * as os from 'os'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import * as DebuggerExtension from './Debugger/extension'; -import * as LanguageServer from './LanguageServer/extension'; -import * as util from './common'; -import * as Telemetry from './telemetry'; - -import * as semver from 'semver'; -import { CppToolsApi, CppToolsExtension } from 'vscode-cpptools'; -import * as nls from 'vscode-nls'; -import { TargetPopulation } from 'vscode-tas-client'; -import { CppBuildTaskProvider, cppBuildTaskProvider } from './LanguageServer/cppBuildTaskProvider'; -import { getLocaleId, getLocalizedHtmlPath } from './LanguageServer/localization'; -import { PersistentState } from './LanguageServer/persistentState'; -import { CppSettings } from './LanguageServer/settings'; -import { logAndReturn, returns } from './Utility/Async/returns'; -import { CppTools1 } from './cppTools1'; -import { logMachineIdMappings } from './id'; -import { disposeOutputChannels, log } from './logger'; -import { PlatformInformation } from './platform'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -const cppTools: CppTools1 = new CppTools1(); -let languageServiceDisabled: boolean = false; -let reloadMessageShown: boolean = false; -const disposables: vscode.Disposable[] = []; - -export async function activate(context: vscode.ExtensionContext): Promise { - util.setExtensionContext(context); - Telemetry.activate(); - util.setProgress(0); - - // Register a protocol handler to serve localized versions of the schema for c_cpp_properties.json - class SchemaProvider implements vscode.TextDocumentContentProvider { - public async provideTextDocumentContent(uri: vscode.Uri): Promise { - console.assert(uri.path[0] === '/', "A preceding slash is expected on schema uri path"); - const fileName: string = uri.path.substring(1); - const locale: string = getLocaleId(); - let localizedFilePath: string = util.getExtensionFilePath(path.join("dist/schema/", locale, fileName)); - const fileExists: boolean = await util.checkFileExists(localizedFilePath); - if (!fileExists) { - localizedFilePath = util.getExtensionFilePath(fileName); - } - return util.readFileText(localizedFilePath); - } - } - - vscode.workspace.registerTextDocumentContentProvider('cpptools-schema', new SchemaProvider()); - - // Initialize the DebuggerExtension and register the related commands and providers. - await DebuggerExtension.initialize(context); - - const info: PlatformInformation = await PlatformInformation.GetPlatformInformation(); - sendTelemetry(info); - - // Always attempt to make the binaries executable, not just when installedVersion changes. - // The user may have uninstalled and reinstalled the same version. - await makeBinariesExecutable(); - - // Notify users if debugging may not be supported on their OS. - util.checkDistro(info); - await checkVsixCompatibility(); - LanguageServer.UpdateInsidersAccess(); - - const ignoreRecommendations: boolean | undefined = vscode.workspace.getConfiguration("extensions", null).get("ignoreRecommendations"); - if (ignoreRecommendations !== true) { - await LanguageServer.preReleaseCheck(); - } - - const settings: CppSettings = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined); - let isOldMacOs: boolean = false; - if (info.platform === 'darwin') { - const releaseParts: string[] = os.release().split("."); - if (releaseParts.length >= 1) { - isOldMacOs = parseInt(releaseParts[0]) < 16; - } - } - - // Read the setting and determine whether we should activate the language server prior to installing callbacks, - // to ensure there is no potential race condition. LanguageServer.activate() is called near the end of this - // function, to allow any further setup to occur here, prior to activation. - const isIntelliSenseEngineDisabled: boolean = settings.intelliSenseEngine === "disabled"; - const shouldActivateLanguageServer: boolean = !isIntelliSenseEngineDisabled && !isOldMacOs; - - if (isOldMacOs) { - languageServiceDisabled = true; - void vscode.window.showErrorMessage(localize("macos.version.deprecated", "Versions of the C/C++ extension more recent than {0} require at least macOS version {1}.", "1.9.8", "10.12")); - } else { - if (settings.intelliSenseEngine === "disabled") { - languageServiceDisabled = true; - } - let currentIntelliSenseEngineValue: string | undefined = settings.intelliSenseEngine; - disposables.push(vscode.workspace.onDidChangeConfiguration(() => { - const settings: CppSettings = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined); - if (!reloadMessageShown && settings.intelliSenseEngine !== currentIntelliSenseEngineValue) { - if (currentIntelliSenseEngineValue === "disabled") { - // If switching from disabled to enabled, we can continue activation. - currentIntelliSenseEngineValue = settings.intelliSenseEngine; - languageServiceDisabled = false; - return LanguageServer.activate(); - } else { - // We can't deactivate or change engines on the fly, so prompt for window reload. - reloadMessageShown = true; - void util.promptForReloadWindowDueToSettingsChange(); - } - } - })); - } - - if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { - for (let i: number = 0; i < vscode.workspace.workspaceFolders.length; ++i) { - const config: string = path.join(vscode.workspace.workspaceFolders[i].uri.fsPath, ".vscode/c_cpp_properties.json"); - if (await util.checkFileExists(config)) { - const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(config); - void vscode.languages.setTextDocumentLanguage(doc, "jsonc"); - util.setWorkspaceIsCpp(); - } - } - } - - disposables.push(vscode.tasks.registerTaskProvider(CppBuildTaskProvider.CppBuildScriptType, cppBuildTaskProvider)); - - vscode.tasks.onDidStartTask(event => { - if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType - || event.execution.task.name.startsWith(LanguageServer.configPrefix)) { - Telemetry.logLanguageServerEvent('buildTaskStarted'); - } - }); - - vscode.tasks.onDidEndTask(event => { - if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType - || event.execution.task.name.startsWith(LanguageServer.configPrefix)) { - Telemetry.logLanguageServerEvent('buildTaskFinished'); - } - }); - - if (shouldActivateLanguageServer) { - await LanguageServer.activate(); - } else if (isIntelliSenseEngineDisabled) { - await LanguageServer.registerCommands(false); - // The check here for isIntelliSenseEngineDisabled avoids logging - // the message on old Macs that we've already displayed a warning for. - log(localize("intellisense.disabled", "intelliSenseEngine is disabled")); - } - - return cppTools; -} - -export async function deactivate(): Promise { - DebuggerExtension.dispose(); - void Telemetry.deactivate().catch(returns.undefined); - disposables.forEach(d => d.dispose()); - if (languageServiceDisabled) { - return; - } - await LanguageServer.deactivate(); - disposeOutputChannels(); -} - -async function makeBinariesExecutable(): Promise { - const promises: Thenable[] = []; - if (process.platform !== 'win32') { - const commonBinaries: string[] = [ - "./bin/cpptools", - "./bin/cpptools-srv", - "./bin/cpptools-wordexp", - "./LLVM/bin/clang-format", - "./LLVM/bin/clang-tidy", - "./debugAdapters/bin/OpenDebugAD7" - ]; - commonBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); - if (process.platform === "darwin") { - const macBinaries: string[] = [ - "./debugAdapters/lldb-mi/bin/lldb-mi" - ]; - macBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); - if (os.arch() === "x64") { - const oldMacBinaries: string[] = [ - "./debugAdapters/lldb/bin/debugserver", - "./debugAdapters/lldb/bin/lldb-mi", - "./debugAdapters/lldb/bin/lldb-argdumper", - "./debugAdapters/lldb/bin/lldb-launcher" - ]; - oldMacBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); - } - } else if (os.arch() === "x64") { - promises.push(util.allowExecution(util.getExtensionFilePath("./bin/libc.so"))); - } - } - await Promise.all(promises); -} - -function sendTelemetry(info: PlatformInformation): void { - const telemetryProperties: { [key: string]: string } = {}; - if (info.distribution) { - telemetryProperties['linuxDistroName'] = info.distribution.name; - telemetryProperties['linuxDistroVersion'] = info.distribution.version; - } - telemetryProperties['osArchitecture'] = os.arch(); - telemetryProperties['infoArchitecture'] = info.architecture; - const targetPopulation: TargetPopulation = util.getCppToolsTargetPopulation(); - switch (targetPopulation) { - case TargetPopulation.Public: - telemetryProperties['targetPopulation'] = "Public"; - break; - case TargetPopulation.Internal: - telemetryProperties['targetPopulation'] = "Internal"; - break; - case TargetPopulation.Insiders: - telemetryProperties['targetPopulation'] = "Insiders"; - break; - default: - break; - } - Telemetry.logDebuggerEvent("acquisition", telemetryProperties); - logMachineIdMappings().catch(logAndReturn.undefined); -} - -async function checkVsixCompatibility(): Promise { - const ignoreMismatchedCompatibleVsix: PersistentState = new PersistentState("CPP." + util.packageJson.version + ".ignoreMismatchedCompatibleVsix", false); - let resetIgnoreMismatchedCompatibleVsix: boolean = true; - - // Check to ensure the correct platform-specific VSIX was installed. - const vsixManifestPath: string = path.join(util.extensionPath, ".vsixmanifest"); - // Skip the check if the file does not exist, such as when debugging cpptools. - if (await util.checkFileExists(vsixManifestPath)) { - const content: string = await util.readFileText(vsixManifestPath); - const matches: RegExpMatchArray | null = content.match(/TargetPlatform="(?[^"]*)"/); - if (matches && matches.length > 0 && matches.groups) { - const vsixTargetPlatform: string = matches.groups['platform']; - const platformInfo: PlatformInformation = await PlatformInformation.GetPlatformInformation(); - let isPlatformCompatible: boolean = true; - let isPlatformMatching: boolean = true; - switch (vsixTargetPlatform) { - case "win32-x64": - isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "x64"; - // x64 binaries can also be run on arm64 Windows 11. - isPlatformCompatible = platformInfo.platform === "win32" && (platformInfo.architecture === "x64" || (platformInfo.architecture === "arm64" && semver.gte(os.release(), "10.0.22000"))); - break; - case "win32-ia32": - isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "x86"; - // x86 binaries can also be run on x64 and arm64 Windows. - isPlatformCompatible = platformInfo.platform === "win32" && (platformInfo.architecture === "x86" || platformInfo.architecture === "x64" || platformInfo.architecture === "arm64"); - break; - case "win32-arm64": - isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "arm64"; - isPlatformCompatible = isPlatformMatching; - break; - case "linux-x64": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "x64" && platformInfo.distribution?.name !== "alpine"; - isPlatformCompatible = isPlatformMatching; - break; - case "linux-arm64": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm64" && platformInfo.distribution?.name !== "alpine"; - isPlatformCompatible = isPlatformMatching; - break; - case "linux-armhf": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm" && platformInfo.distribution?.name !== "alpine"; - // armhf binaries can also be run on aarch64 linux. - isPlatformCompatible = platformInfo.platform === "linux" && (platformInfo.architecture === "arm" || platformInfo.architecture === "arm64") && platformInfo.distribution?.name !== "alpine"; - break; - case "alpine-x64": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "x64" && platformInfo.distribution?.name === "alpine"; - isPlatformCompatible = isPlatformMatching; - break; - case "alpine-arm64": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm64" && platformInfo.distribution?.name === "alpine"; - isPlatformCompatible = isPlatformMatching; - break; - case "darwin-x64": - isPlatformMatching = platformInfo.platform === "darwin" && platformInfo.architecture === "x64"; - isPlatformCompatible = isPlatformMatching; - break; - case "darwin-arm64": - isPlatformMatching = platformInfo.platform === "darwin" && platformInfo.architecture === "arm64"; - // x64 binaries can also be run on arm64 macOS. - isPlatformCompatible = platformInfo.platform === "darwin" && (platformInfo.architecture === "x64" || platformInfo.architecture === "arm64"); - break; - default: - console.log("Unrecognized TargetPlatform in .vsixmanifest"); - break; - } - const moreInfoButton: string = localize("more.info.button", "More Info"); - const ignoreButton: string = localize("ignore.button", "Ignore"); - let promise: Thenable | undefined; - if (!isPlatformCompatible) { - promise = vscode.window.showErrorMessage(localize("vsix.platform.incompatible", "The C/C++ extension installed does not match your system.", vsixTargetPlatform), moreInfoButton); - } else if (!isPlatformMatching) { - if (!ignoreMismatchedCompatibleVsix.Value) { - resetIgnoreMismatchedCompatibleVsix = false; - promise = vscode.window.showWarningMessage(localize("vsix.platform.mismatching", "The C/C++ extension installed is compatible with but does not match your system.", vsixTargetPlatform), moreInfoButton, ignoreButton); - } - } - - void promise?.then((value) => { - if (value === moreInfoButton) { - void vscode.commands.executeCommand("markdown.showPreview", vscode.Uri.file(getLocalizedHtmlPath("Reinstalling the Extension.md"))); - } else if (value === ignoreButton) { - ignoreMismatchedCompatibleVsix.Value = true; - } - }, logAndReturn.undefined); - } else { - console.log("Unable to find TargetPlatform in .vsixmanifest"); - } - } - if (resetIgnoreMismatchedCompatibleVsix) { - ignoreMismatchedCompatibleVsix.Value = false; - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import * as os from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as DebuggerExtension from './Debugger/extension'; +import * as LanguageServer from './LanguageServer/extension'; +import * as util from './common'; +import * as Telemetry from './telemetry'; + +import * as semver from 'semver'; +import { CppToolsApi, CppToolsExtension } from 'vscode-cpptools'; +import * as nls from 'vscode-nls'; +import { TargetPopulation } from 'vscode-tas-client'; +import { CppBuildTaskProvider, cppBuildTaskProvider } from './LanguageServer/cppBuildTaskProvider'; +import { getLocaleId, getLocalizedHtmlPath } from './LanguageServer/localization'; +import { PersistentState } from './LanguageServer/persistentState'; +import { CppSettings } from './LanguageServer/settings'; +import { logAndReturn, returns } from './Utility/Async/returns'; +import { CppTools1 } from './cppTools1'; +import { logMachineIdMappings } from './id'; +import { sendInstrumentation } from './instrumentation'; +import { disposeOutputChannels, log } from './logger'; +import { PlatformInformation } from './platform'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +const cppTools: CppTools1 = new CppTools1(); +let languageServiceDisabled: boolean = false; +let reloadMessageShown: boolean = false; +const disposables: vscode.Disposable[] = []; + +export async function activate(context: vscode.ExtensionContext): Promise { + sendInstrumentation({ + name: "activate", + context: { cpptools: '', start: '' } + }); + + util.setExtensionContext(context); + Telemetry.activate(); + util.setProgress(0); + + // Register a protocol handler to serve localized versions of the schema for c_cpp_properties.json + class SchemaProvider implements vscode.TextDocumentContentProvider { + public async provideTextDocumentContent(uri: vscode.Uri): Promise { + console.assert(uri.path[0] === '/', "A preceding slash is expected on schema uri path"); + const fileName: string = uri.path.substring(1); + const locale: string = getLocaleId(); + let localizedFilePath: string = util.getExtensionFilePath(path.join("dist/schema/", locale, fileName)); + const fileExists: boolean = await util.checkFileExists(localizedFilePath); + if (!fileExists) { + localizedFilePath = util.getExtensionFilePath(fileName); + } + return util.readFileText(localizedFilePath); + } + } + + vscode.workspace.registerTextDocumentContentProvider('cpptools-schema', new SchemaProvider()); + + // Initialize the DebuggerExtension and register the related commands and providers. + await DebuggerExtension.initialize(context); + + const info: PlatformInformation = await PlatformInformation.GetPlatformInformation(); + sendTelemetry(info); + + // Always attempt to make the binaries executable, not just when installedVersion changes. + // The user may have uninstalled and reinstalled the same version. + await makeBinariesExecutable(); + + // Notify users if debugging may not be supported on their OS. + util.checkDistro(info); + await checkVsixCompatibility(); + LanguageServer.UpdateInsidersAccess(); + + const ignoreRecommendations: boolean | undefined = vscode.workspace.getConfiguration("extensions", null).get("ignoreRecommendations"); + if (ignoreRecommendations !== true) { + await LanguageServer.preReleaseCheck(); + } + + const settings: CppSettings = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined); + let isOldMacOs: boolean = false; + if (info.platform === 'darwin') { + const releaseParts: string[] = os.release().split("."); + if (releaseParts.length >= 1) { + isOldMacOs = parseInt(releaseParts[0]) < 16; + } + } + + // Read the setting and determine whether we should activate the language server prior to installing callbacks, + // to ensure there is no potential race condition. LanguageServer.activate() is called near the end of this + // function, to allow any further setup to occur here, prior to activation. + const isIntelliSenseEngineDisabled: boolean = settings.intelliSenseEngine === "disabled"; + const shouldActivateLanguageServer: boolean = !isIntelliSenseEngineDisabled && !isOldMacOs; + + if (isOldMacOs) { + languageServiceDisabled = true; + void vscode.window.showErrorMessage(localize("macos.version.deprecated", "Versions of the C/C++ extension more recent than {0} require at least macOS version {1}.", "1.9.8", "10.12")); + } else { + if (settings.intelliSenseEngine === "disabled") { + languageServiceDisabled = true; + } + let currentIntelliSenseEngineValue: string | undefined = settings.intelliSenseEngine; + disposables.push(vscode.workspace.onDidChangeConfiguration(() => { + const settings: CppSettings = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined); + if (!reloadMessageShown && settings.intelliSenseEngine !== currentIntelliSenseEngineValue) { + if (currentIntelliSenseEngineValue === "disabled") { + // If switching from disabled to enabled, we can continue activation. + currentIntelliSenseEngineValue = settings.intelliSenseEngine; + languageServiceDisabled = false; + return LanguageServer.activate(); + } else { + // We can't deactivate or change engines on the fly, so prompt for window reload. + reloadMessageShown = true; + void util.promptForReloadWindowDueToSettingsChange(); + } + } + })); + } + + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + for (let i: number = 0; i < vscode.workspace.workspaceFolders.length; ++i) { + const config: string = path.join(vscode.workspace.workspaceFolders[i].uri.fsPath, ".vscode/c_cpp_properties.json"); + if (await util.checkFileExists(config)) { + const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(config); + void vscode.languages.setTextDocumentLanguage(doc, "jsonc"); + util.setWorkspaceIsCpp(); + } + } + } + + disposables.push(vscode.tasks.registerTaskProvider(CppBuildTaskProvider.CppBuildScriptType, cppBuildTaskProvider)); + + vscode.tasks.onDidStartTask(event => { + if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType + || event.execution.task.name.startsWith(LanguageServer.configPrefix)) { + Telemetry.logLanguageServerEvent('buildTaskStarted'); + } + }); + + vscode.tasks.onDidEndTask(event => { + if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType + || event.execution.task.name.startsWith(LanguageServer.configPrefix)) { + Telemetry.logLanguageServerEvent('buildTaskFinished'); + } + }); + + if (shouldActivateLanguageServer) { + await LanguageServer.activate(); + } else if (isIntelliSenseEngineDisabled) { + await LanguageServer.registerCommands(false); + // The check here for isIntelliSenseEngineDisabled avoids logging + // the message on old Macs that we've already displayed a warning for. + log(localize("intellisense.disabled", "intelliSenseEngine is disabled")); + } + + sendInstrumentation({ + name: "activate", + context: { cpptools: '', end: '' } + }); + return cppTools; +} + +export async function deactivate(): Promise { + DebuggerExtension.dispose(); + void Telemetry.deactivate().catch(returns.undefined); + disposables.forEach(d => d.dispose()); + if (languageServiceDisabled) { + return; + } + await LanguageServer.deactivate(); + disposeOutputChannels(); +} + +async function makeBinariesExecutable(): Promise { + const promises: Thenable[] = []; + if (process.platform !== 'win32') { + const commonBinaries: string[] = [ + "./bin/cpptools", + "./bin/cpptools-srv", + "./bin/cpptools-wordexp", + "./LLVM/bin/clang-format", + "./LLVM/bin/clang-tidy", + "./debugAdapters/bin/OpenDebugAD7" + ]; + commonBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); + if (process.platform === "darwin") { + const macBinaries: string[] = [ + "./debugAdapters/lldb-mi/bin/lldb-mi" + ]; + macBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); + if (os.arch() === "x64") { + const oldMacBinaries: string[] = [ + "./debugAdapters/lldb/bin/debugserver", + "./debugAdapters/lldb/bin/lldb-mi", + "./debugAdapters/lldb/bin/lldb-argdumper", + "./debugAdapters/lldb/bin/lldb-launcher" + ]; + oldMacBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); + } + } else if (os.arch() === "x64") { + promises.push(util.allowExecution(util.getExtensionFilePath("./bin/libc.so"))); + } + } + await Promise.all(promises); +} + +function sendTelemetry(info: PlatformInformation): void { + const telemetryProperties: { [key: string]: string } = {}; + if (info.distribution) { + telemetryProperties['linuxDistroName'] = info.distribution.name; + telemetryProperties['linuxDistroVersion'] = info.distribution.version; + } + telemetryProperties['osArchitecture'] = os.arch(); + telemetryProperties['infoArchitecture'] = info.architecture; + const targetPopulation: TargetPopulation = util.getCppToolsTargetPopulation(); + switch (targetPopulation) { + case TargetPopulation.Public: + telemetryProperties['targetPopulation'] = "Public"; + break; + case TargetPopulation.Internal: + telemetryProperties['targetPopulation'] = "Internal"; + break; + case TargetPopulation.Insiders: + telemetryProperties['targetPopulation'] = "Insiders"; + break; + default: + break; + } + Telemetry.logDebuggerEvent("acquisition", telemetryProperties); + logMachineIdMappings().catch(logAndReturn.undefined); +} + +async function checkVsixCompatibility(): Promise { + const ignoreMismatchedCompatibleVsix: PersistentState = new PersistentState("CPP." + util.packageJson.version + ".ignoreMismatchedCompatibleVsix", false); + let resetIgnoreMismatchedCompatibleVsix: boolean = true; + + // Check to ensure the correct platform-specific VSIX was installed. + const vsixManifestPath: string = path.join(util.extensionPath, ".vsixmanifest"); + // Skip the check if the file does not exist, such as when debugging cpptools. + if (await util.checkFileExists(vsixManifestPath)) { + const content: string = await util.readFileText(vsixManifestPath); + const matches: RegExpMatchArray | null = content.match(/TargetPlatform="(?[^"]*)"/); + if (matches && matches.length > 0 && matches.groups) { + const vsixTargetPlatform: string = matches.groups['platform']; + const platformInfo: PlatformInformation = await PlatformInformation.GetPlatformInformation(); + let isPlatformCompatible: boolean = true; + let isPlatformMatching: boolean = true; + switch (vsixTargetPlatform) { + case "win32-x64": + isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "x64"; + // x64 binaries can also be run on arm64 Windows 11. + isPlatformCompatible = platformInfo.platform === "win32" && (platformInfo.architecture === "x64" || (platformInfo.architecture === "arm64" && semver.gte(os.release(), "10.0.22000"))); + break; + case "win32-ia32": + isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "x86"; + // x86 binaries can also be run on x64 and arm64 Windows. + isPlatformCompatible = platformInfo.platform === "win32" && (platformInfo.architecture === "x86" || platformInfo.architecture === "x64" || platformInfo.architecture === "arm64"); + break; + case "win32-arm64": + isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "arm64"; + isPlatformCompatible = isPlatformMatching; + break; + case "linux-x64": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "x64" && platformInfo.distribution?.name !== "alpine"; + isPlatformCompatible = isPlatformMatching; + break; + case "linux-arm64": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm64" && platformInfo.distribution?.name !== "alpine"; + isPlatformCompatible = isPlatformMatching; + break; + case "linux-armhf": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm" && platformInfo.distribution?.name !== "alpine"; + // armhf binaries can also be run on aarch64 linux. + isPlatformCompatible = platformInfo.platform === "linux" && (platformInfo.architecture === "arm" || platformInfo.architecture === "arm64") && platformInfo.distribution?.name !== "alpine"; + break; + case "alpine-x64": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "x64" && platformInfo.distribution?.name === "alpine"; + isPlatformCompatible = isPlatformMatching; + break; + case "alpine-arm64": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm64" && platformInfo.distribution?.name === "alpine"; + isPlatformCompatible = isPlatformMatching; + break; + case "darwin-x64": + isPlatformMatching = platformInfo.platform === "darwin" && platformInfo.architecture === "x64"; + isPlatformCompatible = isPlatformMatching; + break; + case "darwin-arm64": + isPlatformMatching = platformInfo.platform === "darwin" && platformInfo.architecture === "arm64"; + // x64 binaries can also be run on arm64 macOS. + isPlatformCompatible = platformInfo.platform === "darwin" && (platformInfo.architecture === "x64" || platformInfo.architecture === "arm64"); + break; + default: + console.log("Unrecognized TargetPlatform in .vsixmanifest"); + break; + } + const moreInfoButton: string = localize("more.info.button", "More Info"); + const ignoreButton: string = localize("ignore.button", "Ignore"); + let promise: Thenable | undefined; + if (!isPlatformCompatible) { + promise = vscode.window.showErrorMessage(localize("vsix.platform.incompatible", "The C/C++ extension installed does not match your system.", vsixTargetPlatform), moreInfoButton); + } else if (!isPlatformMatching) { + if (!ignoreMismatchedCompatibleVsix.Value) { + resetIgnoreMismatchedCompatibleVsix = false; + promise = vscode.window.showWarningMessage(localize("vsix.platform.mismatching", "The C/C++ extension installed is compatible with but does not match your system.", vsixTargetPlatform), moreInfoButton, ignoreButton); + } + } + + void promise?.then((value) => { + if (value === moreInfoButton) { + void vscode.commands.executeCommand("markdown.showPreview", vscode.Uri.file(getLocalizedHtmlPath("Reinstalling the Extension.md"))); + } else if (value === ignoreButton) { + ignoreMismatchedCompatibleVsix.Value = true; + } + }, logAndReturn.undefined); + } else { + console.log("Unable to find TargetPlatform in .vsixmanifest"); + } + } + if (resetIgnoreMismatchedCompatibleVsix) { + ignoreMismatchedCompatibleVsix.Value = false; + } +} diff --git a/Extension/src/platform.ts b/Extension/src/platform.ts index 5b5e3c1ab8..686ff7b4c2 100644 --- a/Extension/src/platform.ts +++ b/Extension/src/platform.ts @@ -1,105 +1,105 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - -import * as fs from 'fs'; -import * as os from 'os'; -import * as plist from 'plist'; -import * as nls from 'vscode-nls'; -import { LinuxDistribution } from './linuxDistribution'; -import * as logger from './logger'; -import { SessionState, SupportedWindowsVersions } from './sessionState'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -export function GetOSName(processPlatform: string | undefined): string | undefined { - switch (processPlatform) { - case "win32": return "Windows"; - case "darwin": return "macOS"; - case "linux": return "Linux"; - default: return undefined; - } -} - -export class PlatformInformation { - constructor(public platform: string, public architecture: string, public distribution?: LinuxDistribution, public version?: string) { } - - public static async GetPlatformInformation(): Promise { - const platform: string = os.platform(); - const architecture: string = PlatformInformation.GetArchitecture(); - let distribution: LinuxDistribution | undefined; - let version: string | undefined; - switch (platform) { - case "win32": - version = PlatformInformation.GetWindowsVersion(); - void SessionState.windowsVersion.set(version as SupportedWindowsVersions); - break; - case "linux": - distribution = await LinuxDistribution.GetDistroInformation(); - break; - case "darwin": - version = await PlatformInformation.GetDarwinVersion(); - break; - default: - throw new Error(localize("unknown.os.platform", "Unknown OS platform")); - } - - return new PlatformInformation(platform, architecture, distribution, version); - } - - public static GetArchitecture(): string { - const arch: string = os.arch(); - switch (arch) { - case "arm64": - case "arm": - return arch; - case "x32": - case "ia32": - return "x86"; - default: - return "x64"; - } - } - - private static GetDarwinVersion(): Promise { - const DARWIN_SYSTEM_VERSION_PLIST: string = "/System/Library/CoreServices/SystemVersion.plist"; - let productDarwinVersion: string = ""; - let errorMessage: string = ""; - - if (fs.existsSync(DARWIN_SYSTEM_VERSION_PLIST)) { - const systemVersionPListBuffer: Buffer = fs.readFileSync(DARWIN_SYSTEM_VERSION_PLIST); - const systemVersionData: any = plist.parse(systemVersionPListBuffer.toString()); - if (systemVersionData) { - productDarwinVersion = systemVersionData.ProductVersion; - } else { - errorMessage = localize("missing.plist.productversion", "Could not get ProduceVersion from SystemVersion.plist"); - } - } else { - errorMessage = localize("missing.darwin.systemversion.file", "Failed to find SystemVersion.plist in {0}.", DARWIN_SYSTEM_VERSION_PLIST); - } - - if (errorMessage) { - logger.getOutputChannel().appendLine(errorMessage); - logger.showOutputChannel(); - } - - return Promise.resolve(productDarwinVersion); - } - - private static GetWindowsVersion(): SupportedWindowsVersions { - const version = os.release().split('.'); - if (version.length > 0) { - if (version[0] === '10') { - if (version.length > 2 && version[2].startsWith('1')) { - // 10.0.10240 - 10.0.190## - return '10'; - } - // 10.0.22000+ - return '11'; - } - } - return ''; - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as plist from 'plist'; +import * as nls from 'vscode-nls'; +import { LinuxDistribution } from './linuxDistribution'; +import { getOutputChannelLogger, showOutputChannel } from './logger'; +import { SessionState, SupportedWindowsVersions } from './sessionState'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export function GetOSName(processPlatform: string | undefined): string | undefined { + switch (processPlatform) { + case "win32": return "Windows"; + case "darwin": return "macOS"; + case "linux": return "Linux"; + default: return undefined; + } +} + +export class PlatformInformation { + constructor(public platform: string, public architecture: string, public distribution?: LinuxDistribution, public version?: string) { } + + public static async GetPlatformInformation(): Promise { + const platform: string = os.platform(); + const architecture: string = PlatformInformation.GetArchitecture(); + let distribution: LinuxDistribution | undefined; + let version: string | undefined; + switch (platform) { + case "win32": + version = PlatformInformation.GetWindowsVersion(); + void SessionState.windowsVersion.set(version as SupportedWindowsVersions); + break; + case "linux": + distribution = await LinuxDistribution.GetDistroInformation(); + break; + case "darwin": + version = await PlatformInformation.GetDarwinVersion(); + break; + default: + throw new Error(localize("unknown.os.platform", "Unknown OS platform")); + } + + return new PlatformInformation(platform, architecture, distribution, version); + } + + public static GetArchitecture(): string { + const arch: string = os.arch(); + switch (arch) { + case "arm64": + case "arm": + return arch; + case "x32": + case "ia32": + return "x86"; + default: + return "x64"; + } + } + + private static GetDarwinVersion(): Promise { + const DARWIN_SYSTEM_VERSION_PLIST: string = "/System/Library/CoreServices/SystemVersion.plist"; + let productDarwinVersion: string = ""; + let errorMessage: string = ""; + + if (fs.existsSync(DARWIN_SYSTEM_VERSION_PLIST)) { + const systemVersionPListBuffer: Buffer = fs.readFileSync(DARWIN_SYSTEM_VERSION_PLIST); + const systemVersionData: any = plist.parse(systemVersionPListBuffer.toString()); + if (systemVersionData) { + productDarwinVersion = systemVersionData.ProductVersion; + } else { + errorMessage = localize("missing.plist.productversion", "Could not get ProduceVersion from SystemVersion.plist"); + } + } else { + errorMessage = localize("missing.darwin.systemversion.file", "Failed to find SystemVersion.plist in {0}.", DARWIN_SYSTEM_VERSION_PLIST); + } + + if (errorMessage) { + getOutputChannelLogger().appendLine(errorMessage); + showOutputChannel(); + } + + return Promise.resolve(productDarwinVersion); + } + + private static GetWindowsVersion(): SupportedWindowsVersions { + const version = os.release().split('.'); + if (version.length > 0) { + if (version[0] === '10') { + if (version.length > 2 && version[2].startsWith('1')) { + // 10.0.10240 - 10.0.190## + return '10'; + } + // 10.0.22000+ + return '11'; + } + } + return ''; + } +}