diff --git a/appservice/package-lock.json b/appservice/package-lock.json index 60b6613b3f..9296415b8e 100644 --- a/appservice/package-lock.json +++ b/appservice/package-lock.json @@ -1718,7 +1718,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -1739,12 +1740,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1759,17 +1762,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -1886,7 +1892,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -1898,6 +1905,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1912,6 +1920,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1919,12 +1928,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -1943,6 +1954,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2030,7 +2042,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2042,6 +2055,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2127,7 +2141,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2163,6 +2178,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2182,6 +2198,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2225,12 +2242,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -3752,6 +3771,40 @@ "pinkie": "^2.0.0" } }, + "portfinder": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz", + "integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==", + "dev": true, + "requires": { + "async": "^1.5.2", + "debug": "^2.2.0", + "mkdirp": "0.5.x" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", diff --git a/appservice/package.json b/appservice/package.json index f901fcfa54..4f2a80e678 100644 --- a/appservice/package.json +++ b/appservice/package.json @@ -60,6 +60,7 @@ "mocha": "^5.2.0", "mocha-junit-reporter": "^1.22.0", "mocha-multi-reporters": "^1.1.7", + "portfinder": "^1.0.20", "tslint": "^5.16.0", "tslint-microsoft-contrib": "5.0.1", "typescript": "^3.4.5", diff --git a/appservice/src/index.ts b/appservice/src/index.ts index 23bc1e4994..c7d197cdf4 100644 --- a/appservice/src/index.ts +++ b/appservice/src/index.ts @@ -13,6 +13,9 @@ export * from './createAppService/SiteHostingPlanStep'; export * from './createAppService/SiteNameStep'; export * from './createAppService/SiteOSStep'; export * from './createAppService/SiteRuntimeStep'; +export * from './remoteDebug/remoteDebugCommon'; +export * from './remoteDebug/startRemoteDebug'; +export * from './remoteDebug/stopRemoteDebug'; export * from './createSlot'; export * from './deploy/deploy'; export * from './deploy/runPreDeployTask'; diff --git a/appservice/src/remoteDebug/remoteDebugCommon.ts b/appservice/src/remoteDebug/remoteDebugCommon.ts new file mode 100644 index 0000000000..aa5e28177d --- /dev/null +++ b/appservice/src/remoteDebug/remoteDebugCommon.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SiteConfigResource } from 'azure-arm-website/lib/models'; +import * as vscode from 'vscode'; +import { callWithTelemetryAndErrorHandling, DialogResponses, IActionContext } from 'vscode-azureextensionui'; +import { ext } from '../extensionVariables'; +import { SiteClient } from '../SiteClient'; + +export const remoteDebugLink: string = 'https://aka.ms/appsvc-remotedebug'; + +export function reportMessage(message: string, progress: vscode.Progress<{}>): void { + ext.outputChannel.appendLine(message); + progress.report({ message: message }); +} + +export async function setRemoteDebug(isRemoteDebuggingToBeEnabled: boolean, confirmMessage: string, noopMessage: string | undefined, siteClient: SiteClient, siteConfig: SiteConfigResource, progress?: vscode.Progress<{}>, learnMoreLink?: string): Promise { + if (isRemoteDebuggingToBeEnabled !== siteConfig.remoteDebuggingEnabled) { + const confirmButton: vscode.MessageItem = isRemoteDebuggingToBeEnabled ? { title: 'Enable' } : { title: 'Disable' }; + + // don't have to check input as this handles cancels and learnMore responses + await ext.ui.showWarningMessage(confirmMessage, { modal: true, learnMoreLink }, confirmButton, DialogResponses.cancel); + siteConfig.remoteDebuggingEnabled = isRemoteDebuggingToBeEnabled; + if (progress) { + reportMessage('Updating site configuration to set remote debugging...', progress); + } + + await callWithTelemetryAndErrorHandling('appService.remoteDebugUpdateConfiguration', async (context: IActionContext) => { + context.errorHandling.suppressDisplay = true; + context.errorHandling.rethrow = true; + await siteClient.updateConfiguration(siteConfig); + }); + + if (progress) { + reportMessage('Updating site configuration done.', progress); + } + } else { + // Update not needed + if (noopMessage) { + vscode.window.showWarningMessage(noopMessage); + } + } +} diff --git a/appservice/src/remoteDebug/startRemoteDebug.ts b/appservice/src/remoteDebug/startRemoteDebug.ts new file mode 100644 index 0000000000..00c7cc4628 --- /dev/null +++ b/appservice/src/remoteDebug/startRemoteDebug.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SiteConfigResource, User } from 'azure-arm-website/lib/models'; +import * as portfinder from 'portfinder'; +import * as vscode from 'vscode'; +import { callWithTelemetryAndErrorHandling, IActionContext } from 'vscode-azureextensionui'; +import { ext } from '../extensionVariables'; +import { SiteClient } from '../SiteClient'; +import { TunnelProxy } from '../TunnelProxy'; +import { remoteDebugLink, reportMessage, setRemoteDebug } from './remoteDebugCommon'; + +let isRemoteDebugging: boolean = false; + +export async function startRemoteDebug(siteClient: SiteClient, siteConfig: SiteConfigResource): Promise { + if (isRemoteDebugging) { + throw new Error('Azure Remote Debugging is currently starting or already started.'); + } + + isRemoteDebugging = true; + try { + await startRemoteDebugInternal(siteClient, siteConfig); + } catch (error) { + isRemoteDebugging = false; + throw error; + } +} + +async function startRemoteDebugInternal(siteClient: SiteClient, siteConfig: SiteConfigResource): Promise { + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (progress: vscode.Progress<{}>): Promise => { + const debugConfig: vscode.DebugConfiguration = await getDebugConfiguration(); + // tslint:disable-next-line:no-unsafe-any + const localHostPortNumber: number = debugConfig.port; + + reportMessage('Checking app settings...', progress); + + const confirmEnableMessage: string = 'The app configuration will be updated to enable remote debugging and restarted. Would you like to continue?'; + await setRemoteDebug(true, confirmEnableMessage, undefined, siteClient, siteConfig, progress, remoteDebugLink); + + reportMessage('Starting tunnel proxy...', progress); + + const publishCredential: User = await siteClient.getWebAppPublishCredential(); + const tunnelProxy: TunnelProxy = new TunnelProxy(localHostPortNumber, siteClient, publishCredential); + await callWithTelemetryAndErrorHandling('appService.remoteDebugStartProxy', async (startContext: IActionContext) => { + startContext.errorHandling.suppressDisplay = true; + startContext.errorHandling.rethrow = true; + await tunnelProxy.startProxy(); + }); + + reportMessage('Attaching debugger...', progress); + + await callWithTelemetryAndErrorHandling('appService.remoteDebugAttach', async (attachContext: IActionContext) => { + attachContext.errorHandling.suppressDisplay = true; + attachContext.errorHandling.rethrow = true; + await vscode.debug.startDebugging(undefined, debugConfig); + }); + + reportMessage('Attached!', progress); + + const terminateDebugListener: vscode.Disposable = vscode.debug.onDidTerminateDebugSession(async (event: vscode.DebugSession) => { + if (event.name === debugConfig.name) { + isRemoteDebugging = false; + + if (tunnelProxy !== undefined) { + tunnelProxy.dispose(); + } + terminateDebugListener.dispose(); + + const confirmDisableMessage: string = 'Leaving the app in debugging mode may cause performance issues. Would you like to disable debugging for this app? The app will be restarted.'; + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (innerProgress: vscode.Progress<{}>): Promise => { + await setRemoteDebug(false, confirmDisableMessage, undefined, siteClient, siteConfig, innerProgress, remoteDebugLink); + }); + } + }); + }); +} + +async function getDebugConfiguration(): Promise { + const sessionId: string = Date.now().toString(); + const portNumber: number = await portfinder.getPortPromise(); + + // So far only node is supported + const config: vscode.DebugConfiguration = { + // return { + name: sessionId, + type: 'node', + protocol: 'inspector', + request: 'attach', + address: 'localhost', + port: portNumber + }; + + // Try to map workspace folder source files to the remote instance + if (vscode.workspace.workspaceFolders) { + if (vscode.workspace.workspaceFolders.length === 1) { + config.localRoot = vscode.workspace.workspaceFolders[0].uri.fsPath; + config.remoteRoot = '/home/site/wwwroot'; + } else { + // In this case we don't know which folder to use. Show a warning and proceed. + // In the future we should allow users to choose a workspace folder to map sources from. + // tslint:disable-next-line:no-floating-promises + ext.ui.showWarningMessage('Unable to bind breakpoints from workspace when multiple folders are open. Use "loaded scripts" instead.'); + } + } else { + // vscode will throw an error if you try to start debugging without any workspace folder open + throw new Error('Please open a workspace folder before attaching a debugger.'); + } + + return config; +} diff --git a/appservice/src/remoteDebug/stopRemoteDebug.ts b/appservice/src/remoteDebug/stopRemoteDebug.ts new file mode 100644 index 0000000000..e741f40fe9 --- /dev/null +++ b/appservice/src/remoteDebug/stopRemoteDebug.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SiteConfigResource } from 'azure-arm-website/lib/models'; +import * as vscode from 'vscode'; +import { SiteClient } from '../SiteClient'; +import { remoteDebugLink, setRemoteDebug } from './remoteDebugCommon'; + +export async function stopRemoteDebug(siteClient: SiteClient, siteConfig: SiteConfigResource): Promise { + const confirmMessage: string = 'The app configuration will be updated to disable remote debugging and restarted. Would you like to continue?'; + const noopMessage: string = 'The app is not configured for debugging.'; + + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (progress: vscode.Progress<{}>): Promise => { + await setRemoteDebug(false, confirmMessage, noopMessage, siteClient, siteConfig, progress, remoteDebugLink); + }); +}