Add support for installing azdata on Windows (#11387)

* Add support for installing azdata on Windows

* Don't run startup code when in test context since it blocks on UI input

* restart checks

* Disable calls for now
This commit is contained in:
Charles Gagnon
2020-07-18 18:15:52 -07:00
committed by GitHub
parent 5613a97fae
commit 6f9991e22b
7 changed files with 652 additions and 3 deletions

View File

@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as path from 'path';
import * as uuid from 'uuid';
import * as which from 'which';
import * as vscode from 'vscode';
import { HttpClient } from './common/httpClient';
import * as loc from './localizedConstants';
import { executeCommand } from './common/childProcess';
/**
* Information about an azdata installation
*/
export interface IAzdata {
path: string,
version: string
}
/**
* Finds the existing installation of azdata, or throws an error if it couldn't find it
* or encountered an unexpected error.
* @param outputChannel Channel used to display diagnostic information
*/
export async function findAzdata(outputChannel: vscode.OutputChannel): Promise<IAzdata> {
outputChannel.appendLine(loc.searchingForAzdata);
try {
let azdata: IAzdata | undefined = undefined;
switch (process.platform) {
case 'darwin':
azdata = await findAzdataDarwin(outputChannel);
break;
case 'win32':
azdata = await findAzdataWin32(outputChannel);
break;
default:
azdata = await findSpecificAzdata('azdata', outputChannel);
}
outputChannel.appendLine(loc.foundExistingAzdata(azdata.path, azdata.version));
return azdata;
} catch (err) {
outputChannel.appendLine(loc.couldNotFindAzdata(err));
throw err;
}
}
/**
* Downloads the appropriate installer and/or runs the command to install azdata
* @param outputChannel Channel used to display diagnostic information
*/
export async function downloadAndInstallAzdata(outputChannel: vscode.OutputChannel): Promise<void> {
const statusDisposable = vscode.window.setStatusBarMessage(loc.installingAzdata);
try {
switch (process.platform) {
case 'win32': await downloadAndInstallAzdataWin32(outputChannel);
}
} finally {
statusDisposable.dispose();
}
}
/**
* Downloads the Windows installer and runs it
* @param outputChannel Channel used to display diagnostic information
*/
async function downloadAndInstallAzdataWin32(outputChannel: vscode.OutputChannel): Promise<void> {
const downloadPath = path.join(os.tmpdir(), `azdata-msi-${uuid.v4()}.msi`);
outputChannel.appendLine(loc.downloadingTo('azdata-cli.msi', downloadPath));
await HttpClient.download('https://aka.ms/azdata-msi', downloadPath, outputChannel);
await executeCommand('msiexec', ['/i', downloadPath], outputChannel);
}
/**
* Finds azdata specifically on Windows
* @param outputChannel Channel used to display diagnostic information
*/
async function findAzdataWin32(outputChannel: vscode.OutputChannel): Promise<IAzdata> {
const whichPromise = new Promise<string>((c, e) => which('azdata.cmd', (err, path) => err ? e(err) : c(path)));
return findSpecificAzdata(await whichPromise, outputChannel);
}
/**
* Finds azdata specifically on MacOS
* @param outputChannel Channel used to display diagnostic information
*/
async function findAzdataDarwin(_outputChannel: vscode.OutputChannel): Promise<IAzdata> {
throw new Error('Not yet implemented');
}
/**
* Gets the version using a known azdata path
* @param path The path to the azdata executable
* @param outputChannel Channel used to display diagnostic information
*/
async function findSpecificAzdata(path: string, outputChannel: vscode.OutputChannel): Promise<IAzdata> {
const versionOutput = await executeCommand(path, ['--version'], outputChannel);
return {
path: path,
version: parseVersion(versionOutput)
};
}
/**
* Parses out the azdata version from the raw azdata version output
* @param raw The raw version output from azdata --version
*/
function parseVersion(raw: string): string {
// Currently the version is a multi-line string that contains other version information such
// as the Python installation, with the first line being the version of azdata itself.
const lines = raw.split(os.EOL);
return lines[0].trim();
}

View File

@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as cp from 'child_process';
import * as loc from '../localizedConstants';
/**
* Wrapper error for when an unexpected exit code was recieved
*/
export class ExitCodeError extends Error {
constructor(public code: number) {
super(`Unexpected exit code ${code}`);
}
}
/**
*
* @param command The command to execute
* @param args Optional args to pass, every arg and arg value must be a separate item in the array
* @param outputChannel Channel used to display diagnostic information
*/
export async function executeCommand(command: string, args?: string[], outputChannel?: vscode.OutputChannel): Promise<string> {
return new Promise((resolve, reject) => {
outputChannel?.appendLine(loc.executingCommand(command, args ?? []));
const buffers: Buffer[] = [];
const child = cp.spawn(command, args);
child.stdout.on('data', (b: Buffer) => buffers.push(b));
child.on('error', reject);
child.on('exit', code => code ? reject(new ExitCodeError(code)) : resolve(Buffer.concat(buffers).toString('utf8').trim()));
});
}

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as request from 'request';
import * as loc from '../localizedConstants';
const DownloadTimeout = 20000;
export namespace HttpClient {
/**
* Downloads a file from the given URL
* @param downloadUrl The URL to download the file from
* @param targetPath The path to download the file to
* @param outputChannel Channel used to display diagnostic information
*/
export function download(downloadUrl: string, targetPath: string, outputChannel: vscode.OutputChannel): Promise<void> {
return new Promise((resolve, reject) => {
let totalMegaBytes: number | undefined = undefined;
let receivedBytes = 0;
let printThreshold = 0.1;
let downloadRequest = request.get(downloadUrl, { timeout: DownloadTimeout })
.on('error', downloadError => {
outputChannel.appendLine(loc.downloadError);
outputChannel.appendLine(downloadError?.message ?? downloadError);
reject(downloadError);
})
.on('response', (response) => {
if (response.statusCode !== 200) {
outputChannel.appendLine(loc.downloadError);
outputChannel.appendLine(response.statusMessage);
return reject(response.statusMessage);
}
let contentLength = response.headers['content-length'];
let totalBytes = parseInt(contentLength || '0');
totalMegaBytes = totalBytes / (1024 * 1024);
outputChannel.appendLine(loc.downloadingProgressMb('0', totalMegaBytes.toFixed(2)));
})
.on('data', (data) => {
receivedBytes += data.length;
if (totalMegaBytes) {
let receivedMegaBytes = receivedBytes / (1024 * 1024);
let percentage = receivedMegaBytes / totalMegaBytes;
if (percentage >= printThreshold) {
outputChannel.appendLine(loc.downloadingProgressMb(receivedMegaBytes.toFixed(2), totalMegaBytes.toFixed(2)));
printThreshold += 0.1;
}
}
});
downloadRequest.pipe(fs.createWriteStream(targetPath))
.on('close', async () => {
outputChannel.appendLine(loc.downloadFinished);
resolve();
})
.on('error', (downloadError) => {
reject(downloadError);
downloadRequest.abort();
});
});
}
}

View File

@@ -3,8 +3,36 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { findAzdata, downloadAndInstallAzdata } from './azdata';
import * as loc from './localizedConstants';
import { ExitCodeError } from './common/childProcess';
export async function activate(): Promise<void> {
const outputChannel = vscode.window.createOutputChannel('azdata');
if (false) {
await checkForAzdata(outputChannel);
}
}
export function deactivate(): void {
async function checkForAzdata(outputChannel: vscode.OutputChannel): Promise<void> {
try {
const azdata = await findAzdata(outputChannel);
vscode.window.showInformationMessage(loc.foundExistingAzdata(azdata.path, azdata.version));
} catch (err) {
const response = await vscode.window.showErrorMessage(loc.couldNotFindAzdataWithPrompt, loc.install, loc.cancel);
if (response === loc.install) {
try {
await downloadAndInstallAzdata(outputChannel);
vscode.window.showInformationMessage(loc.azdataInstalled);
} catch (err) {
// 1602 is User Cancelling installation - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.installError(err));
}
}
}
}
}
export function deactivate(): void { }

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export const searchingForAzdata = localize('azdata.searchingForAzdata', "Searching for existing azdata installation...");
export function foundExistingAzdata(path: string, version: string): string { return localize('azdata.foundExistingAzdata', "Found existing azdata installation at {0} (v{1})", path, version); }
export function downloadingProgressMb(currentMb: string, totalMb: string): string { return localize('azdata.downloadingProgressMb', "Downloading ({0} / {1} MB)", currentMb, totalMb); }
export const downloadFinished = localize('azdata.downloadFinished', "Download finished");
export const install = localize('azdata.install', "Install");
export const installingAzdata = localize('azdata.installingAzdata', "Installing azdata...");
export const azdataInstalled = localize('azdata.azdataInstalled', "azdata was successfully installed. Restarting Azure Data Studio is required to complete configuration - features will not be activated until this is done.");
export const cancel = localize('azdata.cancel', "Cancel");
export function downloadingTo(name: string, location: string): string { return localize('azdata.downloadingTo', "Downloading {0} to {1}", name, location); }
export function couldNotFindAzdata(err: any): string { return localize('azdata.couldNotFindAzdata', "Could not find azdata. Error : {0}", err.message ?? err); }
export const couldNotFindAzdataWithPrompt = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find azdata, install it now? If not then some features will not be able to function.");
export const downloadError = localize('azdata.downloadError', "Error while downloading");
export function installError(err: any): string { return localize('azdata.installError', "Error installing azdata : {0}", err.message ?? err); }
export function executingCommand(command: string, args: string[]): string { return localize('azdata.executingCommand', "Executing command \"{0} {1}\"", command, args?.join(' ')); }