diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a4777c6..7eeb40601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to the "azurefunctions" extension will be documented in this file. -## 0.10.0 - 2018-07-19 +## 0.10.0 - 2018-07-24 ### Added @@ -16,6 +16,7 @@ All notable changes to the "azurefunctions" extension will be documented in this ### Changed +- Debug config for JavaScript functions has changed. See https://aka.ms/AA1vrxa for more info - New C# projects will deploy the result of a 'dotnet publish' rather than deploying the build output - Azure Function Apps created through VS Code will automatically match the runtime from your local machine rather than always using v1 diff --git a/docs/debugConfig.md b/docs/debugConfig.md new file mode 100644 index 000000000..9d9d8f1a0 --- /dev/null +++ b/docs/debugConfig.md @@ -0,0 +1,99 @@ +# Breaking changes to JavaScript Debug Configuration + +The debug configuration for v2 of the Azure Functions runtime [has changed](https://github.com/Azure/azure-functions-core-tools/issues/521) and old configurations will likely fail with the error "Cannot connect to runtime process, timeout after 10000 ms". You will automatically be prompted to update your configuration when you open a project. However, you can manually update your project with one of the following options: + +The first option is to add `languageWorkers:node:arguments` to your `runFunctionsHost` task in `.vscode\tasks.json` as seen below: + +```json +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run Functions Host", + "identifier": "runFunctionsHost", + "type": "shell", + "command": "func host start", + "options": { + "env": { + "languageWorkers:node:arguments": "--inspect=5858" + } + }, + "isBackground": true, + "presentation": { + "reveal": "always" + }, + "problemMatcher": [ + { + "owner": "azureFunctions", + "pattern": [ + { + "regexp": "\\b\\B", + "file": 1, + "location": 2, + "message": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "^.*Stopping host.*", + "endsPattern": "^.*Job host started.*" + } + } + ] + } + ] +} +``` + +The second option requires two steps: + +1. Pass the `--language-worker` parameter to `func host start` in your `runFunctionsHost` task in `.vscode\tasks.json` as seen below: + ```json + { + "version": "2.0.0", + "tasks": [ + { + "label": "Run Functions Host", + "identifier": "runFunctionsHost", + "type": "shell", + "command": "func host start --language-worker -- \"--inspect=5858\"", + "isBackground": true, + "presentation": { + "reveal": "always" + }, + "problemMatcher": [ + { + "owner": "azureFunctions", + "pattern": [ + { + "regexp": "\\b\\B", + "file": 1, + "location": 2, + "message": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "^.*Stopping host.*", + "endsPattern": "^.*Job host started.*" + } + } + ] + } + ] + } + ``` + +1. Add the `FUNCTIONS_WORKER_RUNTIME` setting to your `local.settings.json`: + + ```json + { + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "", + "FUNCTIONS_WORKER_RUNTIME": "node" + } + } + ``` + +The recommended debug configuration going forward has not been finalized. It will likely be similar to Option 2, but without a change to `local.settings.json` (since that file is not tracked by git). See [here](https://github.com/Azure/azure-functions-host/issues/3120) for more information. diff --git a/package.json b/package.json index 93d2fa31e..29d673364 100644 --- a/package.json +++ b/package.json @@ -564,6 +564,11 @@ "description": "%azFunc.showProjectWarningDescription%", "default": true }, + "azureFunctions.showDebugConfigWarning": { + "type": "boolean", + "description": "%azFunc.showDebugConfigWarningDescription%", + "default": true + }, "azureFunctions.pickProcessTimeout": { "type": "integer", "description": "%azFunc.pickProcessTimeoutDescription%", diff --git a/package.nls.json b/package.nls.json index 816031d32..438e1bbb9 100644 --- a/package.nls.json +++ b/package.nls.json @@ -33,6 +33,7 @@ "azFunc.showCoreToolsWarningDescription": "Show a warning if your installed version of Azure Functions Core Tools is out-of-date.", "azFunc.show64BitWarningDescription": "Show a warning to install a 64-bit version of the Azure Functions Core Tools when you create a .NET Framework project.", "azFunc.showProjectWarningDescription": "Show a warning when an Azure Functions project was detected that has not been initialized for use in VS Code.", + "azFunc.showDebugConfigWarningDescription": "Show a warning when an Azure Functions project was detected that has an out-of-date debug configuration.", "azFunc.startStreamingLogs": "Start Streaming Logs", "azFunc.stopStreamingLogs": "Stop Streaming Logs", "azFunc.enableRemoteDebugging": "Enable remote debugging, an experimental feature that only supports Java-based Functions Apps.", diff --git a/src/commands/createNewProject/IProjectCreator.ts b/src/commands/createNewProject/IProjectCreator.ts index 87d7a80ae..1e148a9d2 100644 --- a/src/commands/createNewProject/IProjectCreator.ts +++ b/src/commands/createNewProject/IProjectCreator.ts @@ -40,7 +40,7 @@ export abstract class ProjectCreatorBase { * Add all project files not included in the '.vscode' folder */ public abstract addNonVSCodeFiles(): Promise; - public abstract getTasksJson(): {} | Promise<{}>; + public abstract getTasksJson(runtime: string): {} | Promise<{}>; public getRecommendedExtensions(): string[] { return ['ms-azuretools.vscode-azurefunctions']; } diff --git a/src/commands/createNewProject/ITasksJson.ts b/src/commands/createNewProject/ITasksJson.ts new file mode 100644 index 000000000..bfbc4a5f8 --- /dev/null +++ b/src/commands/createNewProject/ITasksJson.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface ITasksJson { + tasks: ITask[]; +} + +export interface ITask { + identifier: string; + options?: ITaskOptions; +} + +export interface ITaskOptions { + env?: { + [key: string]: string; + }; +} diff --git a/src/commands/createNewProject/JavaScriptProjectCreator.ts b/src/commands/createNewProject/JavaScriptProjectCreator.ts index 0583bb459..a2f6d45cf 100644 --- a/src/commands/createNewProject/JavaScriptProjectCreator.ts +++ b/src/commands/createNewProject/JavaScriptProjectCreator.ts @@ -3,11 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TemplateFilter } from "../../constants"; +import { ProjectRuntime, TemplateFilter } from "../../constants"; import { localize } from "../../localize"; -import { funcHostTaskId } from "./IProjectCreator"; +import { funcHostProblemMatcher, funcHostTaskId, funcHostTaskLabel } from "./IProjectCreator"; +import { ITaskOptions } from "./ITasksJson"; import { ScriptProjectCreatorBase } from './ScriptProjectCreatorBase'; +export const funcNodeDebugArgs: string = '--inspect=5858'; +export const funcNodeDebugEnvVar: string = 'languageWorkers:node:arguments'; + export class JavaScriptProjectCreator extends ScriptProjectCreatorBase { public readonly templateFilter: TemplateFilter = TemplateFilter.Verified; @@ -28,4 +32,33 @@ export class JavaScriptProjectCreator extends ScriptProjectCreatorBase { ] }; } + + public getTasksJson(runtime: string): {} { + let options: ITaskOptions | undefined; + if (runtime !== ProjectRuntime.one) { + options = {}; + options.env = {}; + options.env[funcNodeDebugEnvVar] = funcNodeDebugArgs; + } + + return { + version: '2.0.0', + tasks: [ + { + label: funcHostTaskLabel, + identifier: funcHostTaskId, + type: 'shell', + command: 'func host start', + options: options, + isBackground: true, + presentation: { + reveal: 'always' + }, + problemMatcher: [ + funcHostProblemMatcher + ] + } + ] + }; + } } diff --git a/src/commands/createNewProject/ScriptProjectCreatorBase.ts b/src/commands/createNewProject/ScriptProjectCreatorBase.ts index 562d63cdf..02a4a447c 100644 --- a/src/commands/createNewProject/ScriptProjectCreatorBase.ts +++ b/src/commands/createNewProject/ScriptProjectCreatorBase.ts @@ -6,7 +6,6 @@ import * as fse from 'fs-extra'; import * as path from 'path'; import { gitignoreFileName, hostFileName, localSettingsFileName, ProjectRuntime, TemplateFilter } from '../../constants'; -import { tryGetLocalRuntimeVersion } from '../../funcCoreTools/tryGetLocalRuntimeVersion'; import { ILocalAppSettings } from '../../LocalAppSettings'; import { confirmOverwriteFile } from "../../utils/fs"; import * as fsUtil from '../../utils/fs'; @@ -47,12 +46,7 @@ export class ScriptProjectCreatorBase extends ProjectCreatorBase { public readonly templateFilter: TemplateFilter = TemplateFilter.All; public readonly functionsWorkerRuntime: string | undefined; - public async getRuntime(): Promise { - // tslint:disable-next-line:strict-boolean-expressions - return await tryGetLocalRuntimeVersion() || ScriptProjectCreatorBase.defaultRuntime; - } - - public getTasksJson(): {} { + public getTasksJson(_runtime: string): {} { return { version: '2.0.0', tasks: [ diff --git a/src/commands/createNewProject/initProjectForVSCode.ts b/src/commands/createNewProject/initProjectForVSCode.ts index 8907e0dae..ac6e54eb4 100644 --- a/src/commands/createNewProject/initProjectForVSCode.ts +++ b/src/commands/createNewProject/initProjectForVSCode.ts @@ -49,7 +49,7 @@ export async function initProjectForVSCode(telemetryProperties: TelemetryPropert const vscodePath: string = path.join(functionAppPath, '.vscode'); await fse.ensureDir(vscodePath); outputChannel.appendLine(localize('writingDebugConfig', 'Writing project debug configuration...')); - await writeDebugConfiguration(projectCreator, vscodePath, ui); + await writeDebugConfiguration(projectCreator, vscodePath, ui, runtime); outputChannel.appendLine(localize('writingSettings', 'Writing project settings...')); await writeVSCodeSettings(projectCreator, vscodePath, runtime, language, templateFilter, ui); outputChannel.appendLine(localize('writingRecommendations', 'Writing extension recommendations...')); @@ -68,10 +68,10 @@ export async function initProjectForVSCode(telemetryProperties: TelemetryPropert return projectCreator; } -async function writeDebugConfiguration(projectCreator: ProjectCreatorBase, vscodePath: string, ui: IAzureUserInput): Promise { +async function writeDebugConfiguration(projectCreator: ProjectCreatorBase, vscodePath: string, ui: IAzureUserInput, runtime: string): Promise { const tasksJsonPath: string = path.join(vscodePath, 'tasks.json'); if (await confirmOverwriteFile(tasksJsonPath, ui)) { - await fsUtil.writeFormattedJson(tasksJsonPath, await projectCreator.getTasksJson()); + await fsUtil.writeFormattedJson(tasksJsonPath, await projectCreator.getTasksJson(runtime)); } const launchJson: {} | undefined = projectCreator.getLaunchJson(); diff --git a/src/commands/createNewProject/validateFunctionProjects.ts b/src/commands/createNewProject/validateFunctionProjects.ts index a4d29bfcc..66087952f 100644 --- a/src/commands/createNewProject/validateFunctionProjects.ts +++ b/src/commands/createNewProject/validateFunctionProjects.ts @@ -9,10 +9,16 @@ import opn = require("opn"); import * as path from 'path'; import * as vscode from 'vscode'; import { DialogResponses, IActionContext, IAzureUserInput } from 'vscode-azureextensionui'; -import { gitignoreFileName, hostFileName, localSettingsFileName, projectLanguageSetting, projectRuntimeSetting } from '../../constants'; +import { gitignoreFileName, hostFileName, localSettingsFileName, ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting, tasksFileName, vscodeFolderName } from '../../constants'; +import { ext } from '../../extensionVariables'; +import { tryGetLocalRuntimeVersion } from '../../funcCoreTools/tryGetLocalRuntimeVersion'; import { localize } from '../../localize'; -import { getFuncExtensionSetting, updateGlobalSetting } from '../../ProjectSettings'; +import { getFuncExtensionSetting, updateGlobalSetting, updateWorkspaceSetting } from '../../ProjectSettings'; +import * as fsUtil from '../../utils/fs'; import { initProjectForVSCode } from './initProjectForVSCode'; +import { funcHostTaskId } from './IProjectCreator'; +import { ITask, ITasksJson } from './ITasksJson'; +import { funcNodeDebugArgs, funcNodeDebugEnvVar } from './JavaScriptProjectCreator'; export async function validateFunctionProjects(actionContext: IActionContext, ui: IAzureUserInput, outputChannel: vscode.OutputChannel, folders: vscode.WorkspaceFolder[] | undefined): Promise { actionContext.suppressTelemetry = true; @@ -24,6 +30,8 @@ export async function validateFunctionProjects(actionContext: IActionContext, ui if (isInitializedProject(folderPath)) { actionContext.properties.isInitialized = 'true'; + actionContext.suppressErrorDisplay = true; // Swallow errors when verifying debug config. No point in showing an error if we can't understand the project anyways + await verifyDebugConfigIsValid(folderPath, actionContext); } else { actionContext.properties.isInitialized = 'false'; if (await promptToInitializeProject(ui, folderPath)) { @@ -68,3 +76,67 @@ function isInitializedProject(folderPath: string): boolean { const runtime: string | undefined = getFuncExtensionSetting(projectRuntimeSetting, folderPath); return !!language && !!runtime; } + +/** + * JavaScript debugging in the func cli had breaking changes in v2.0.1-beta.30. This verifies users are up-to-date with the latest working debug config. + * See https://aka.ms/AA1vrxa for more info + */ +async function verifyDebugConfigIsValid(folderPath: string, actionContext: IActionContext): Promise { + const language: string | undefined = getFuncExtensionSetting(projectLanguageSetting, folderPath); + if (language === ProjectLanguage.JavaScript) { + const localProjectRuntime: ProjectRuntime | undefined = await tryGetLocalRuntimeVersion(); + if (localProjectRuntime === ProjectRuntime.beta) { + const tasksJsonPath: string = path.join(folderPath, vscodeFolderName, tasksFileName); + const rawTasksData: string = (await fse.readFile(tasksJsonPath)).toString(); + + if (!rawTasksData.includes(funcNodeDebugEnvVar)) { + const tasksContent: ITasksJson = JSON.parse(rawTasksData); + + const funcTask: ITask | undefined = tasksContent.tasks.find((t: ITask) => t.identifier === funcHostTaskId); + if (funcTask) { + actionContext.properties.debugConfigValid = 'false'; + + if (await promptToUpdateDebugConfiguration(folderPath)) { + // tslint:disable-next-line:strict-boolean-expressions + funcTask.options = funcTask.options || {}; + // tslint:disable-next-line:strict-boolean-expressions + funcTask.options.env = funcTask.options.env || {}; + funcTask.options.env[funcNodeDebugEnvVar] = funcNodeDebugArgs; + await fsUtil.writeFormattedJson(tasksJsonPath, tasksContent); + + actionContext.properties.updatedDebugConfig = 'true'; + + const viewFile: vscode.MessageItem = { title: 'View file' }; + const result: vscode.MessageItem | undefined = await vscode.window.showInformationMessage(localize('tasksUpdated', 'Your "tasks.json" file has been updated.'), viewFile); + if (result === viewFile) { + await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(vscode.Uri.file(tasksJsonPath))); + } + } + } + } + } + } +} + +async function promptToUpdateDebugConfiguration(fsPath: string): Promise { + const settingKey: string = 'showDebugConfigWarning'; + if (getFuncExtensionSetting(settingKey)) { + const updateConfig: vscode.MessageItem = { title: localize('updateTasks', 'Update tasks.json') }; + const message: string = localize('uninitializedWarning', 'Your debug configuration is out of date and may not work with the latest version of the Azure Functions Core Tools.'); + let result: vscode.MessageItem; + do { + result = await ext.ui.showWarningMessage(message, updateConfig, DialogResponses.dontWarnAgain, DialogResponses.learnMore); + if (result === DialogResponses.dontWarnAgain) { + await updateWorkspaceSetting(settingKey, false, fsPath); + } else if (result === DialogResponses.learnMore) { + // don't wait to re-show dialog + // tslint:disable-next-line:no-floating-promises + opn('https://aka.ms/AA1vrxa'); + } else { + return true; + } + } while (result === DialogResponses.learnMore); + } + + return false; +} diff --git a/src/constants.ts b/src/constants.ts index 061cf5a8e..16e927c22 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -49,6 +49,8 @@ export enum Platform { export const hostFileName: string = 'host.json'; export const localSettingsFileName: string = 'local.settings.json'; export const proxiesFileName: string = 'proxies.json'; +export const tasksFileName: string = 'tasks.json'; +export const vscodeFolderName: string = '.vscode'; export const gitignoreFileName: string = '.gitignore'; export enum PackageManager {