Feat/tool install master merge back to master (#7819)

* add install tools button (#7454)

* add install tools button

* address comments

* remove description for install tools hint message

* First working version of AutoDeployment of tools (#7647)

First working version of AutoDeployment of tools.

This pull request adds feature to install the tools needed for doing BDC/TINA deployments.

This has been tested so far only on win32 and testing on other platforms is in progress.

* removing TODO and redundant code

* Not localizing azuredatastudio product name

* convert methods returning Promises to async-await

* changing from null to undefined

* Localize all the command labels

* using existing sudo-prompt typings

* progres/error status in ModalDialogue && PR fixes

* review feedback to change warning to information

* revert settings.json changes

* fix resource-Deployment Extension Unit Test

* ensuring platform service's working directory

* incorporate review feedback

* review feedback

* addressing PR feedback

* PR fixes

* PR Feedback

* remove debug logs

* disable UI deployment containers when installing

* addding data type to stdout/stderr messaging

* remove commented code

* revert accidental change

* addressing review feedback

* fix failed install with zero exit code

* fixing bug due to typo

* fixes for linux

* Misc fixes during mac testing

* PR fixes
This commit is contained in:
Arvind Ranasaria
2019-10-18 23:17:21 -07:00
committed by GitHub
parent a2f105a913
commit 4dd6db57ee
18 changed files with 893 additions and 141 deletions

View File

@@ -2,62 +2,246 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ToolType, ITool } from '../../interfaces';
import { SemVer } from 'semver';
import { IPlatformService } from '../platformService';
import * as nls from 'vscode-nls';
import { EOL } from 'os';
import { delimiter } from 'path';
import { SemVer } from 'semver';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, ITool, OsType, ToolStatus, ToolType } from '../../interfaces';
import { getErrorMessage } from '../../utils';
import { IPlatformService } from '../platformService';
const localize = nls.loadMessageBundle();
const toolStatusNotInstalled: string = localize('deploymentDialog.ToolStatus.NotInstalled', "Not Installed");
const toolStatusInstalled: string = localize('deploymentDialog.ToolStatus.Installed', "Installed");
const toolStatusInstalling: string = localize('deploymentDialog.ToolStatus.NotInstalling', "Installing ...");
const toolStatusError: string = localize('deploymentDialog.ToolStatus.Error', "Error");
const toolStatusFailed: string = localize('deploymentDialog.ToolStatus.Failed', "Failed");
const toolStatusLocalized: Map<ToolStatus, string> = new Map<ToolStatus, string>([
[ToolStatus.Error, toolStatusError],
[ToolStatus.Installed, toolStatusInstalled],
[ToolStatus.Installing, toolStatusInstalling],
[ToolStatus.NotInstalled, toolStatusNotInstalled],
[ToolStatus.Failed, toolStatusFailed]
]);
export abstract class ToolBase implements ITool {
constructor(private _platformService: IPlatformService) { }
constructor(private _platformService: IPlatformService) {
this._osType = this._platformService.osType();
}
abstract name: string;
abstract displayName: string;
abstract description: string;
abstract type: ToolType;
abstract homePage: string;
protected abstract getVersionFromOutput(output: string): SemVer | undefined;
protected abstract readonly versionCommand: string;
abstract autoInstallSupported: boolean;
abstract readonly allInstallationCommands: Map<OsType, Command[]>;
public get version(): SemVer | undefined {
protected abstract getVersionFromOutput(output: string): SemVer | undefined;
protected readonly _onDidUpdateData = new vscode.EventEmitter<ITool>();
protected readonly uninstallCommand?: string;
protected abstract readonly versionCommand: Command;
protected async getInstallationPath(): Promise<string | undefined> {
return undefined;
}
protected get installationSearchPaths(): (string | undefined)[] {
return [this.storagePath];
}
protected get downloadPath(): string {
return this.storagePath;
}
protected logToOutputChannel(data: string | Buffer, header?: string): void {
this._platformService.logToOutputChannel(data, header); // data and header are localized by caller
}
public get onDidUpdateData(): vscode.Event<ITool> {
return this._onDidUpdateData.event;
}
protected get status(): ToolStatus {
return this._status;
}
protected set status(value: ToolStatus) {
this._status = value;
this._onDidUpdateData.fire(this);
}
public get displayStatus(): string {
return <string>toolStatusLocalized.get(this._status);
}
public get autoInstallRequired(): boolean {
return this.status !== ToolStatus.Installed && this.autoInstallSupported;
}
public get isNotInstalled(): boolean {
return this.status === ToolStatus.NotInstalled;
}
public get isInstalling(): boolean {
return this.status === ToolStatus.Installing;
}
public get needsInstallation(): boolean {
return this.status !== ToolStatus.Installed;
}
public get storagePath(): string {
const storagePath = this._platformService.storagePath();
if (!this._storagePathEnsured) {
this._platformService.ensureDirectoryExists(storagePath);
this._storagePathEnsured = true;
}
return storagePath;
}
public get osType(): OsType {
return this._osType;
}
protected get version(): SemVer | undefined {
return this._version;
}
public get isInstalled(): boolean {
return this._isInstalled;
protected set version(value: SemVer | undefined) {
this._version = value;
this._onDidUpdateData.fire(this);
}
public get fullVersion(): string | undefined {
return this._version && this._version.version;
}
public get statusDescription(): string | undefined {
return this._statusDescription;
}
public loadInformation(): Promise<void> {
if (this._isInstalled) {
return Promise.resolve();
}
this._isInstalled = false;
this._statusDescription = undefined;
this._version = undefined;
this._versionOutput = undefined;
return this._platformService.runCommand(this.versionCommand).then((stdout) => {
this._versionOutput = stdout;
this._version = this.getVersionFromOutput(stdout);
if (this._version) {
this._isInstalled = true;
} else {
throw localize('deployCluster.InvalidToolVersionOutput', "Invalid output received.");
}
}).catch((error) => {
const errorMessage = typeof error === 'string' ? error :
typeof error.message === 'string' ? error.message : '';
this._statusDescription = localize('deployCluster.GetToolVersionError', "Error retrieving version information.{0}Error: {1}{0}stdout: {2} ", EOL, errorMessage, this._versionOutput);
});
protected get installationCommands(): Command[] | undefined {
return this.allInstallationCommands.get(this.osType);
}
private _isInstalled: boolean = false;
protected async getPip3InstallLocation(packageName: string): Promise<string> {
const command = `pip3 show ${packageName}`;
const pip3ShowOutput: string = await this._platformService.runCommand(command, { sudo: false, ignoreError: true });
const installLocation = /^Location\: (.*)$/gim.exec(pip3ShowOutput);
let retValue = installLocation && installLocation[1];
if (retValue === undefined || retValue === null) {
this.logToOutputChannel(` >${command}`); //command is localized by caller
this.logToOutputChannel(localize('toolBase.getPip3InstallationLocation.LocationNotFound', " Could not find 'Location' in the output:"));
this.logToOutputChannel(pip3ShowOutput, localize('toolBase.getPip3InstallationLocation.Output', " output:"));
return '';
} else {
return retValue;
}
}
public get outputChannelName(): string {
return this._platformService.outputChannelName();
}
public showOutputChannel(preserveFocus?: boolean | undefined): void {
this._platformService.showOutputChannel(preserveFocus);
}
public async install(): Promise<void> {
try {
this.status = ToolStatus.Installing;
await this.installCore();
await this.addInstallationSearchPathsToSystemPath();
this.status = await this.updateVersionAndGetStatus();
} catch (error) {
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;
throw error;
}
// Since we just completed installation, the status should be ToolStatus.Installed
// but if it is ToolStatus.NotInstalled then it means that installation failed with 0 exit code.
if (this.status === ToolStatus.NotInstalled) {
this._statusDescription = localize('toolBase.InstallFailed', "Installation commands completed but version of tool '{0}' could not be detected so our installation attempt has failed. Detection Error: {1}{2}Cleaning up previous installations would help.", this.displayName, this._statusDescription, EOL);
if (this.uninstallCommand) {
this._statusDescription += localize('toolBase.ManualUninstallCommand', " A possibly way to uninstall is using this command:{0} >{1}", EOL, this.uninstallCommand);
}
this._statusDescription += localize('toolBase.SeeOutputChannel', "{0}See output channel '{1}' for more details", EOL, this.outputChannelName);
this.status = ToolStatus.Failed;
throw new Error(this._statusDescription);
}
}
protected async installCore() {
const installationCommands: Command[] | undefined = this.installationCommands;
if (!installationCommands || installationCommands.length === 0) {
throw new Error(localize('toolBase.installCore.CannotInstallTool', "Cannot install tool:${0}::${1} as installation commands are unknown", this.displayName, this.description));
}
for (let i: number = 0; i < installationCommands.length; i++) {
await this._platformService.runCommand(installationCommands[i].command,
{
workingDirectory: installationCommands[i].workingDirectory || this.downloadPath,
additionalEnvironmentVariables: installationCommands[i].additionalEnvironmentVariables,
sudo: installationCommands[i].sudo,
commandTitle: installationCommands[i].comment,
ignoreError: installationCommands[i].ignoreError
},
);
}
}
protected async addInstallationSearchPathsToSystemPath(): Promise<void> {
const installationPath = await this.getInstallationPath();
const searchPaths = [installationPath, ...this.installationSearchPaths].filter(path => !!path);
this.logToOutputChannel(localize('toolBase.addInstallationSearchPathsToSystemPath.SearchPaths', "Search Paths for tool '{0}': {1}", this.displayName, JSON.stringify(searchPaths, undefined, '\t'))); //this.displayName is localized and searchPaths are OS filesystem paths.
searchPaths.forEach(installationSearchPath => {
if (process.env.PATH) {
if (!`${delimiter}${process.env.PATH}${delimiter}`.includes(`${delimiter}${installationSearchPath}${delimiter}`)) {
process.env.PATH += `${delimiter}${installationSearchPath}`;
console.log(`Appending to Path -> ${delimiter}${installationSearchPath}`);
}
} else {
process.env.PATH = installationSearchPath;
console.log(`Appending to Path -> '${delimiter}${installationSearchPath}':${delimiter}${installationSearchPath}`);
}
});
}
public async loadInformation(): Promise<void> {
if (this.status === ToolStatus.NotInstalled) {
await this.addInstallationSearchPathsToSystemPath();
this.status = await this.updateVersionAndGetStatus();
}
}
private async updateVersionAndGetStatus(): Promise<ToolStatus> {
const commandOutput = await this._platformService.runCommand(
this.versionCommand.command,
{
workingDirectory: this.versionCommand.workingDirectory,
additionalEnvironmentVariables: this.versionCommand.additionalEnvironmentVariables,
sudo: false,
ignoreError: true
},
);
this.version = this.getVersionFromOutput(commandOutput);
if (this.version) {
return ToolStatus.Installed;
}
else {
this._statusDescription = localize('deployCluster.GetToolVersionError', "Error retrieving version information.{0}Invalid output received, get version command output: '{1}' ", EOL, commandOutput);
return ToolStatus.NotInstalled;
}
}
private _storagePathEnsured: boolean = false;
private _status: ToolStatus = ToolStatus.NotInstalled;
private _osType: OsType;
private _version?: SemVer;
private _statusDescription?: string;
private _versionOutput?: string;
}