mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-11 18:48:33 -05:00
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:
115
extensions/azdata/src/azdata.ts
Normal file
115
extensions/azdata/src/azdata.ts
Normal 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();
|
||||
}
|
||||
34
extensions/azdata/src/common/childProcess.ts
Normal file
34
extensions/azdata/src/common/childProcess.ts
Normal 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()));
|
||||
});
|
||||
}
|
||||
65
extensions/azdata/src/common/httpClient.ts
Normal file
65
extensions/azdata/src/common/httpClient.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
|
||||
22
extensions/azdata/src/localizedConstants.ts
Normal file
22
extensions/azdata/src/localizedConstants.ts
Normal 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(' ')); }
|
||||
Reference in New Issue
Block a user