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
This commit is contained in:
Arvind Ranasaria
2020-01-03 14:18:01 -08:00
committed by GitHub
parent 5b34dd2eee
commit ef5ca7bc3a
5 changed files with 114 additions and 84 deletions

View File

@@ -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<ITool>;

View File

@@ -43,6 +43,7 @@ export class DockerTool extends ToolBase {
}
return version;
}
protected get versionCommand(): Command {
return { command: 'docker version --format "{{json .}}"' };
}

View File

@@ -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<void> {
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<void> {
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;
}

View File

@@ -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<azdata.TableComponentProperties>({
@@ -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<string[]>((returnArray, currentTool) => {
if (currentTool.autoInstallNeeded) {
returnArray.push(...currentTool.dependencyMessages);
}
return returnArray;
}, /* initial Value of return array*/[]);
const informationalMessagesSet: Set<string> = new Set<string>(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<string[]>((returnArray, currentTool) => {
if (currentTool.autoInstallNeeded) {
returnArray.push(...currentTool.dependencyMessages);
}
return returnArray;
}, /* initial Value of return array*/[]);
const informationalMessagesSet: Set<string> = new Set<string>(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<azdata.CheckBoxProperties>({
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];

View File

@@ -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<string> = new Set<string>();
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) {