Skip to content

Commit

Permalink
Refactor remote debugging commands into common library to support fun…
Browse files Browse the repository at this point in the history
…ctions (#522)

* Refactor remote debugging commands into common library so we can support functions

* - Remove "stop remote debugging" command
- Change language to remove references to "web app"
- Check for explicitly stopped apps

* Localize strings
  • Loading branch information
mrcrane authored and nturinski committed Jul 2, 2019
1 parent 0f1e4b6 commit bf243bb
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 28 deletions.
34 changes: 34 additions & 0 deletions appservice/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions appservice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 19 additions & 28 deletions appservice/src/TunnelProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as requestP from 'request-promise';
import { IParsedError, parseError } from 'vscode-azureextensionui';
import * as websocket from 'websocket';
import { ext } from './extensionVariables';
import { localize } from './localize';
import { SiteClient } from './SiteClient';
import { delay } from './utils/delay';

Expand Down Expand Up @@ -120,7 +121,7 @@ class TunnelSocket extends EventEmitter {
/**
* Interface for tunnel GetStatus API
*/
enum WebAppState {
enum AppState {
STARTED = 'STARTED',
STARTING = 'STARTING',
STOPPED = 'STOPPED'
Expand All @@ -129,7 +130,7 @@ enum WebAppState {
interface ITunnelStatus {
port: number;
canReachPort: boolean;
state: WebAppState;
state: AppState;
msg: string;
}

Expand Down Expand Up @@ -171,26 +172,16 @@ export class TunnelProxy {
this._server.unref();
}

// Starts up an app when it is found to be in the STOPPED state
// Apps can be in the STOPPED state for different reasons:
// 1. A stop request was sent through the Azure API (using the portal, using the extension, etc)
// - In this case it will respond with 403 until a start request is sent to the Azure API
// 2. The app is inactive, or was recently started
// - In this case it will stay stopped until a request is made to the app itself, waking it up
//
// To cover both cases, we send a start request followed by a ping to the app url
// Starts up an app by pinging it when it is found to be in the STOPPED state
private async startupApp(): Promise<void> {
ext.outputChannel.appendLine('[WebApp Tunnel] Sending start request...');
await this._client.start();

ext.outputChannel.appendLine('[WebApp Tunnel] Pinging app default url...');
ext.outputChannel.appendLine('[Tunnel] Pinging app default url...');
// tslint:disable-next-line:no-unsafe-any
const pingResponse: IncomingMessage = await requestP.get({
uri: this._client.defaultHostUrl,
simple: false, // allows the call to succeed without exception, even when status code is not 2XX
resolveWithFullResponse: true // allows access to the status code from the response
});
ext.outputChannel.appendLine(`[WebApp Tunnel] Ping responded with status code: ${pingResponse.statusCode}`);
ext.outputChannel.appendLine(`[Tunnel] Ping responded with status code: ${pingResponse.statusCode}`);
}

private async checkTunnelStatus(): Promise<void> {
Expand All @@ -209,32 +200,32 @@ export class TunnelProxy {
try {
// tslint:disable-next-line:no-unsafe-any
const responseBody: string = await requestP.get(statusOptions);
ext.outputChannel.appendLine(`[WebApp Tunnel] Checking status, body: ${responseBody}`);
ext.outputChannel.appendLine(`[Tunnel] Checking status, body: ${responseBody}`);

// tslint:disable-next-line:no-unsafe-any
tunnelStatus = JSON.parse(responseBody);
} catch (error) {
const parsedError: IParsedError = parseError(error);
ext.outputChannel.appendLine(`[WebApp Tunnel] Checking status, error: ${parsedError.message}`);
throw new Error(`Error getting tunnel status: ${parsedError.errorType}`);
ext.outputChannel.appendLine(`[Tunnel] Checking status, error: ${parsedError.message}`);
throw new Error(localize('tunnelStatusError', 'Error getting tunnel status: {0}', parsedError.errorType));
}

if (tunnelStatus.state === WebAppState.STARTED) {
if (tunnelStatus.state === AppState.STARTED) {
if ((tunnelStatus.port === 2222 && !this._isSsh) || (tunnelStatus.port !== 2222 && this._isSsh)) {
// Tunnel is pointed to default SSH port and still needs time to restart
throw new RetryableTunnelStatusError('WebApp is waiting for restart');
throw new RetryableTunnelStatusError();
} else if (tunnelStatus.canReachPort) {
return;
} else {
throw new Error('WebApp is started, but port is unreachable');
throw new Error(localize('tunnelUnreachable', 'App is started, but port is unreachable'));
}
} else if (tunnelStatus.state === WebAppState.STARTING) {
throw new RetryableTunnelStatusError('WebApp is starting');
} else if (tunnelStatus.state === WebAppState.STOPPED) {
} else if (tunnelStatus.state === AppState.STARTING) {
throw new RetryableTunnelStatusError();
} else if (tunnelStatus.state === AppState.STOPPED) {
await this.startupApp();
throw new RetryableTunnelStatusError('WebApp is starting from STOPPED state');
throw new RetryableTunnelStatusError();
} else {
throw new Error(`Unexpected WebApp state: ${tunnelStatus.state}`);
throw new Error(localize('tunnelStatusError', 'Unexpected app state: {0}', tunnelStatus.state));
}
}

Expand All @@ -252,14 +243,14 @@ export class TunnelProxy {
return;
} catch (error) {
if (!(error instanceof RetryableTunnelStatusError)) {
reject(new Error(`Unable to establish connection to application: ${parseError(error).message}`));
reject(new Error(localize('tunnelFailed', 'Unable to establish connection to application: {0}', parseError(error).message)));
return;
} // else allow retry
}

await delay(pollingIntervalMs);
}
reject(new Error('Unable to establish connection to application: Timed out'));
reject(new Error(localize('tunnelTimedOut', 'Unable to establish connection to application: Timed out')));
});
}

Expand Down
2 changes: 2 additions & 0 deletions appservice/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ 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 './createSlot';
export * from './deploy/deploy';
export * from './deploy/runPreDeployTask';
Expand Down
49 changes: 49 additions & 0 deletions appservice/src/remoteDebug/remoteDebugCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* 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 { localize } from '../localize';
import { SiteClient } from '../SiteClient';

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<void> {
const state: string | undefined = await siteClient.getState();
if (state && state.toLowerCase() === 'stopped') {
throw new Error(localize('remoteDebugStopped', 'The app must be running, but is currently in state "Stopped". Start the app to continue.'));
}

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(localize('remoteDebugUpdate', '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(localize('remoteDebugUpdateDone', 'Updating site configuration done.'), progress);
}
} else {
// Update not needed
if (noopMessage) {
vscode.window.showWarningMessage(noopMessage);
}
}
}
113 changes: 113 additions & 0 deletions appservice/src/remoteDebug/startRemoteDebug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* 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 { localize } from '../localize';
import { SiteClient } from '../SiteClient';
import { TunnelProxy } from '../TunnelProxy';
import { reportMessage, setRemoteDebug } from './remoteDebugCommon';

const remoteDebugLink: string = 'https://aka.ms/appsvc-remotedebug';

let isRemoteDebugging: boolean = false;

export async function startRemoteDebug(siteClient: SiteClient, siteConfig: SiteConfigResource): Promise<void> {
if (isRemoteDebugging) {
throw new Error(localize('remoteDebugAlreadyStarted', '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<void> {
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (progress: vscode.Progress<{}>): Promise<void> => {
const debugConfig: vscode.DebugConfiguration = await getDebugConfiguration();
// tslint:disable-next-line:no-unsafe-any
const localHostPortNumber: number = debugConfig.port;

const confirmEnableMessage: string = localize('remoteDebugEnablePrompt', 'The configuration will be updated to enable remote debugging. Would you like to continue? This will restart the app.');
await setRemoteDebug(true, confirmEnableMessage, undefined, siteClient, siteConfig, progress, remoteDebugLink);

reportMessage(localize('remoteDebugStartingTunnel', '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(localize('remoteDebugAttaching', '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(localize('remoteDebugAttached', '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 = localize('remoteDebugDisablePrompt', 'Remaining in debugging mode may cause performance issues. Would you like to disable debugging? This will restart the app.');
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (innerProgress: vscode.Progress<{}>): Promise<void> => {
await setRemoteDebug(false, confirmDisableMessage, undefined, siteClient, siteConfig, innerProgress, remoteDebugLink);
});
}
});
});
}

async function getDebugConfiguration(): Promise<vscode.DebugConfiguration> {
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(localize('remoteDebugMultipleFolders', '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(localize('remoteDebugNoFolders', 'Please open a workspace folder before attaching a debugger.'));
}

return config;
}

0 comments on commit bf243bb

Please sign in to comment.