From ef5ca7bc3a89ae1de768522806c514d0fbf97286 Mon Sep 17 00:00:00 2001 From: Arvind Ranasaria Date: Fri, 3 Jan 2020 14:18:01 -0800 Subject: [PATCH] add handlers for promise rejections (#8735) This change adds to handlers to unexpected promise rejection scenarios. This PR fixes #8640 * add handlers for promise rejections * Displaying all tools load errors * Update toolBase.ts - setting errorMessage to be displayed in the additional information field * disable the select button when tools not discovered * PR fixes --- .../resource-deployment/src/interfaces.ts | 2 +- .../src/services/tools/dockerTool.ts | 1 + .../src/services/tools/toolBase.ts | 32 +++- .../src/ui/resourceTypePickerDialog.ts | 157 +++++++++--------- extensions/resource-deployment/src/utils.ts | 6 +- 5 files changed, 114 insertions(+), 84 deletions(-) diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index 20450b7416..9d5109d999 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -254,7 +254,7 @@ export interface ITool { readonly autoInstallNeeded: boolean; readonly isNotInstalled: boolean; readonly isInstalled: boolean; - readonly installationPath?: string; + readonly installationPathOrAdditionalInformation?: string; readonly outputChannelName: string; readonly fullVersion?: string; readonly onDidUpdateData: vscode.Event; diff --git a/extensions/resource-deployment/src/services/tools/dockerTool.ts b/extensions/resource-deployment/src/services/tools/dockerTool.ts index 19e3d832fc..2fd1649d4c 100644 --- a/extensions/resource-deployment/src/services/tools/dockerTool.ts +++ b/extensions/resource-deployment/src/services/tools/dockerTool.ts @@ -43,6 +43,7 @@ export class DockerTool extends ToolBase { } return version; } + protected get versionCommand(): Command { return { command: 'docker version --format "{{json .}}"' }; } diff --git a/extensions/resource-deployment/src/services/tools/toolBase.ts b/extensions/resource-deployment/src/services/tools/toolBase.ts index e0a933c80f..2efcedb4dd 100644 --- a/extensions/resource-deployment/src/services/tools/toolBase.ts +++ b/extensions/resource-deployment/src/services/tools/toolBase.ts @@ -139,8 +139,8 @@ export abstract class ToolBase implements ITool { return this._statusDescription; } - public get installationPath(): string | undefined { - return this._installationPath; + public get installationPathOrAdditionalInformation(): string | undefined { + return this._installationPathOrAdditionalInformation; } protected get installationCommands(): Command[] | undefined { @@ -186,6 +186,7 @@ export abstract class ToolBase implements ITool { const errorMessage = getErrorMessage(error); this._statusDescription = localize('toolBase.InstallError', "Error installing tool '{0}' [ {1} ].{2}Error: {3}{2}See output channel '{4}' for more details", this.displayName, this.homePage, EOL, errorMessage, this.outputChannelName); this.status = ToolStatus.Error; + this._installationPathOrAdditionalInformation = errorMessage; throw error; } @@ -234,14 +235,33 @@ export abstract class ToolBase implements ITool { }); } + /** + * Sets the tool with discovered state and version information. + * Upon error the this.status field is set to ToolStatus.Error and this.statusDescription && this.installationPathOrAdditionalInformation is set to the corresponding error message + * and original error encountered is re-thrown so that it gets bubbled up to the caller. + */ public async loadInformation(): Promise { - await this._pendingVersionAndStatusUpdate; + try { + await this._pendingVersionAndStatusUpdate; + } catch (error) { + this.status = ToolStatus.Error; + this._statusDescription = getErrorMessage(error); + this._installationPathOrAdditionalInformation = this._statusDescription; + throw error; + } } - private startVersionAndStatusUpdate() { + /** + * Invokes the async method to update version and status for the tool. + */ + private startVersionAndStatusUpdate(): void { + this._statusDescription = ''; this._pendingVersionAndStatusUpdate = this.updateVersionAndStatus(); } + /** + * updates the version and status for the tool. + */ private async updateVersionAndStatus(): Promise { this._statusDescription = ''; await this.addInstallationSearchPathsToSystemPath(); @@ -292,7 +312,7 @@ export abstract class ToolBase implements ITool { if (!commandOutput) { throw new Error(`Install location of tool:'${this.displayName}' could not be discovered`); } else { - this._installationPath = path.resolve(commandOutput.split(EOL)[0]); + this._installationPathOrAdditionalInformation = path.resolve(commandOutput.split(EOL)[0]); } } @@ -304,5 +324,5 @@ export abstract class ToolBase implements ITool { private _status: ToolStatus = ToolStatus.NotInstalled; private _version?: SemVer; private _statusDescription?: string; - private _installationPath?: string; + private _installationPathOrAdditionalInformation?: string; } diff --git a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index 20363d99fe..9e412336ba 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -38,7 +38,7 @@ export class ResourceTypePickerDialog extends DialogBase { this._selectedResourceType = resourceType; this._installToolButton = azdata.window.createButton(localize('deploymentDialog.InstallToolsButton', "Install tools")); this._toDispose.push(this._installToolButton.onClick(() => { - this.installTools(); + this.installTools().catch(error => console.log(error)); })); this._dialogObject.customButtons = [this._installToolButton]; this._installToolButton.hidden = true; @@ -106,10 +106,10 @@ export class ResourceTypePickerDialog extends DialogBase { }; const minVersionColumn: azdata.TableColumn = { value: localize('deploymentDialog.toolMinimumVersionColumnHeader', "Required Version"), - width: 90 + width: 95 }; const installedPathColumn: azdata.TableColumn = { - value: localize('deploymentDialog.toolDiscoveredPathColumnHeader', "Discovered Path"), + value: localize('deploymentDialog.toolDiscoveredPathColumnHeader', "Discovered Path or Additional Information"), width: 570 }; this._toolsTable = view.modelBuilder.table().withProperties({ @@ -197,90 +197,99 @@ export class ResourceTypePickerDialog extends DialogBase { private updateToolsDisplayTable(): void { this.toolRefreshTimestamp = new Date().getTime(); const currentRefreshTimestamp = this.toolRefreshTimestamp; - const toolRequirements = this.getCurrentProvider().requiredTools; const headerRowHeight = 28; - this._toolsTable.height = 25 * Math.max(toolRequirements.length, 1) + headerRowHeight; + this._toolsTable.height = 25 * Math.max(this.toolRequirements.length, 1) + headerRowHeight; this._dialogObject.message = { text: '' }; this._installToolButton.hidden = true; - if (toolRequirements.length === 0) { + if (this.toolRequirements.length === 0) { this._dialogObject.okButton.enabled = true; this._toolsTable.data = [[localize('deploymentDialog.NoRequiredTool', "No tools required"), '']]; this._tools = []; } else { - this._tools = toolRequirements.map(toolReq => { - return this.toolsService.getToolByName(toolReq.name)!; - }); + this._tools = this.toolRequirements.map(toolReq => this.toolsService.getToolByName(toolReq.name)!); this._toolsLoadingComponent.loading = true; this._dialogObject.okButton.enabled = false; + let toolsLoadingErrors: string[] = []; Promise.all( - this._tools.map(tool => tool.loadInformation()) - ).then(async () => { - // If the local timestamp does not match the class level timestamp, it means user has changed options, ignore the results - if (this.toolRefreshTimestamp !== currentRefreshTimestamp) { - return; - } - let minVersionCheckFailed = false; - const toolsToAutoInstall: ITool[] = []; - let messages: string[] = []; - this._toolsTable.data = toolRequirements.map(toolRequirement => { - const tool = this.toolsService.getToolByName(toolRequirement.name)!; - // subscribe to onUpdateData event of the tool. - this._toDispose.push(tool.onDidUpdateData((t: ITool) => { - this.updateToolsDisplayTableData(t); - })); - if (tool.isNotInstalled) { - if (tool.autoInstallSupported) { - toolsToAutoInstall.push(tool); - } else { - messages.push(localize('deploymentDialog.ToolInformation', "'{0}' was not discovered and automated installation is not currently supported. Install '{0}' manually or ensure it is started and discoverable. Once done please restart Azure Data Studio. See [{1}] .", tool.displayName, tool.homePage)); - } - } else if (tool.isInstalled && toolRequirement.version && !tool.isSameOrNewerThan(toolRequirement.version)) { - minVersionCheckFailed = true; - messages.push(localize('deploymentDialog.ToolDoesNotMeetVersionRequirement', "'{0}' [ {1} ] does not meet the minimum version requirement, please uninstall it and restart Azure Data Studio.", tool.displayName, tool.homePage)); - } - - return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', toolRequirement.version || '', tool.installationPath || '']; - }); - - this._installToolButton.hidden = minVersionCheckFailed || (toolsToAutoInstall.length === 0); - this._dialogObject.okButton.enabled = messages.length === 0 && !minVersionCheckFailed && (toolsToAutoInstall.length === 0); - if (messages.length !== 0) { - if (messages.length > 1) { - messages = messages.map(message => `• ${message}`); - } - messages.push(localize('deploymentDialog.VersionInformationDebugHint', "You will need to restart Azure Data Studio if the tools are installed manually after Azure Data Studio is launched to pick up the updated PATH environment variable. You may find additional details in 'Deployments' output channel")); - this._dialogObject.message = { - level: azdata.window.MessageLevel.Error, - text: messages.join(EOL) - }; - } else if ((toolsToAutoInstall.length !== 0) && !this._installationInProgress) { - const installationNeededHeader = toolsToAutoInstall.length === 1 - ? localize('deploymentDialog.InstallToolsHintOne', "Tool: {0} is not installed, you can click the \"{1}\" button to install it.", toolsToAutoInstall[0].displayName, this._installToolButton.label) - : localize('deploymentDialog.InstallToolsHintMany', "Tools: {0} are not installed, you can click the \"{1}\" button to install them.", toolsToAutoInstall.map(t => t.displayName).join(', '), this._installToolButton.label); - let infoText: string[] = [installationNeededHeader]; - const informationalMessagesArray = this._tools.reduce((returnArray, currentTool) => { - if (currentTool.autoInstallNeeded) { - returnArray.push(...currentTool.dependencyMessages); - } - return returnArray; - }, /* initial Value of return array*/[]); - const informationalMessagesSet: Set = new Set(informationalMessagesArray); - if (informationalMessagesSet.size > 0) { - infoText.push(...informationalMessagesSet.values()); - } - // we don't have scenarios that have mixed type of tools - either we don't support auto install: docker, or we support auto install for all required tools - this._dialogObject.message = { - level: azdata.window.MessageLevel.Warning, - text: infoText.join(EOL) - }; - } - this._toolsLoadingComponent.loading = false; - }); + this._tools.map( + tool => tool.loadInformation().catch(() => toolsLoadingErrors.push(`${tool.displayName}:${tool.statusDescription!}`)) + ) + ) + .then(() => this.executeToolsTableWorkflow(currentRefreshTimestamp, toolsLoadingErrors)) + .catch(error => console.log(error)); } } + private executeToolsTableWorkflow(currentRefreshTimestamp: number, toolsLoadingErrors: string[]): void { + // If the local timestamp does not match the class level timestamp, it means user has changed options, ignore the results + if (this.toolRefreshTimestamp !== currentRefreshTimestamp) { + return; + } + let minVersionCheckFailed = false; + const toolsToAutoInstall: ITool[] = []; + let messages: string[] = toolsLoadingErrors!; + this._toolsTable.data = this.toolRequirements.map(toolRequirement => { + const tool = this.toolsService.getToolByName(toolRequirement.name)!; + // subscribe to onUpdateData event of the tool. + this._toDispose.push(tool.onDidUpdateData((t: ITool) => { + this.updateToolsDisplayTableData(t); + })); + if (tool.isNotInstalled) { + if (tool.autoInstallSupported) { + toolsToAutoInstall.push(tool); + } + else { + messages.push(localize('deploymentDialog.ToolInformation', "'{0}' was not discovered and automated installation is not currently supported. Install '{0}' manually or ensure it is started and discoverable. Once done please restart Azure Data Studio. See [{1}] .", tool.displayName, tool.homePage)); + } + } + else if (tool.isInstalled && toolRequirement.version && !tool.isSameOrNewerThan(toolRequirement.version)) { + minVersionCheckFailed = true; + messages.push(localize('deploymentDialog.ToolDoesNotMeetVersionRequirement', "'{0}' [ {1} ] does not meet the minimum version requirement, please uninstall it and restart Azure Data Studio.", tool.displayName, tool.homePage)); + } + return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', toolRequirement.version || '', tool.installationPathOrAdditionalInformation || '']; + }); + this._installToolButton.hidden = minVersionCheckFailed || (toolsToAutoInstall.length === 0); + this._dialogObject.okButton.enabled = messages.length === 0 && !minVersionCheckFailed && (toolsToAutoInstall.length === 0); + if (messages.length !== 0) { + if (messages.length > 1) { + messages = messages.map(message => `• ${message}`); + } + messages.push(localize('deploymentDialog.VersionInformationDebugHint', "You will need to restart Azure Data Studio if the tools are installed manually after Azure Data Studio is launched to pick up the updated PATH environment variable. You may find additional details in 'Deployments' output channel")); + this._dialogObject.message = { + level: azdata.window.MessageLevel.Error, + text: messages.join(EOL) + }; + } + else if ((toolsToAutoInstall.length !== 0) && !this._installationInProgress) { + const installationNeededHeader = toolsToAutoInstall.length === 1 + ? localize('deploymentDialog.InstallToolsHintOne', "Tool: {0} is not installed, you can click the \"{1}\" button to install it.", toolsToAutoInstall[0].displayName, this._installToolButton.label) + : localize('deploymentDialog.InstallToolsHintMany', "Tools: {0} are not installed, you can click the \"{1}\" button to install them.", toolsToAutoInstall.map(t => t.displayName).join(', '), this._installToolButton.label); + let infoText: string[] = [installationNeededHeader]; + const informationalMessagesArray = this._tools.reduce((returnArray, currentTool) => { + if (currentTool.autoInstallNeeded) { + returnArray.push(...currentTool.dependencyMessages); + } + return returnArray; + }, /* initial Value of return array*/[]); + const informationalMessagesSet: Set = new Set(informationalMessagesArray); + if (informationalMessagesSet.size > 0) { + infoText.push(...informationalMessagesSet.values()); + } + // we don't have scenarios that have mixed type of tools - either we don't support auto install: docker, or we support auto install for all required tools + this._dialogObject.message = { + level: azdata.window.MessageLevel.Warning, + text: infoText.join(EOL) + }; + } + this._toolsLoadingComponent.loading = false; + } + + private get toolRequirements() { + return this.getCurrentProvider().requiredTools; + } + private createAgreementCheckbox(agreementInfo: AgreementInfo): azdata.FlexContainer { const checkbox = this._view.modelBuilder.checkBox().withProperties({ ariaLabel: this.getAgreementDisplayText(agreementInfo) @@ -326,7 +335,7 @@ export class ResourceTypePickerDialog extends DialogBase { protected updateToolsDisplayTableData(tool: ITool) { this._toolsTable.data = this._toolsTable.data.map(rowData => { if (rowData[0] === tool.displayName) { - return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', rowData[4]/* required version*/, tool.installationPath || '']; + return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', rowData[4]/* required version*/, tool.installationPathOrAdditionalInformation || '']; } else { return rowData; } @@ -347,7 +356,7 @@ export class ResourceTypePickerDialog extends DialogBase { this._installationInProgress = true; let tool: ITool; try { - const toolRequirements = this.getCurrentProvider().requiredTools; + const toolRequirements = this.toolRequirements; let toolsNotInstalled: ITool[] = []; for (let i: number = 0; i < toolRequirements.length; i++) { const toolReq = toolRequirements[i]; diff --git a/extensions/resource-deployment/src/utils.ts b/extensions/resource-deployment/src/utils.ts index 82d7f0e55e..6d80844764 100644 --- a/extensions/resource-deployment/src/utils.ts +++ b/extensions/resource-deployment/src/utils.ts @@ -25,13 +25,13 @@ export function setEnvironmentVariablesForInstallPaths(tools: ITool[]): void { // Use Set class to make sure the collection only contains unique values. let installationPaths: Set = new Set(); tools.forEach(t => { - if (t.installationPath) { + if (t.installationPathOrAdditionalInformation) { // construct an env variable name with NoteBookEnvironmentVariablePrefix prefix // and tool.name as suffix, making sure of using all uppercase characters and only _ as separator const envVarName = getRuntimeBinaryPathEnvironmentVariableName(t.name); - process.env[envVarName] = t.installationPath; - installationPaths.add(path.dirname(t.installationPath)); + process.env[envVarName] = t.installationPathOrAdditionalInformation; + installationPaths.add(path.dirname(t.installationPathOrAdditionalInformation)); } }); if (installationPaths.size > 0) {