Add azcli extension (#16029)

This commit is contained in:
Charles Gagnon
2021-07-07 13:00:12 -07:00
committed by GitHub
parent 6078e9f459
commit d942799f9d
39 changed files with 4797 additions and 1 deletions

View File

@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AdditionalEnvVars } from 'azdata-ext';
import * as cp from 'child_process';
import * as sudo from 'sudo-prompt';
import * as loc from '../localizedConstants';
import Logger from './logger';
/**
* Wrapper error for when an unexpected exit code was received
*/
export class ExitCodeError extends Error {
constructor(private _code: number, private _stderr: string) {
super();
this.setMessage();
}
public get code(): number {
return this._code;
}
public set code(value: number) {
this._code = value;
}
public get stderr(): string {
return this._stderr;
}
public set stderr(value: string) {
this._stderr = value;
this.setMessage();
}
private setMessage(): void {
this.message = loc.unexpectedExitCode(this._code, this._stderr);
}
}
export type ProcessOutput = { stdout: string, stderr: string };
/**
* Executes the specified command. Throws an error for a non-0 exit code or if stderr receives output
* @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 additionalEnvVars Additional environment variables to add to the process environment
*/
export async function executeCommand(command: string, args: string[], additionalEnvVars?: AdditionalEnvVars): Promise<ProcessOutput> {
return new Promise((resolve, reject) => {
Logger.log(loc.executingCommand(command, args));
const stdoutBuffers: Buffer[] = [];
const stderrBuffers: Buffer[] = [];
const env = Object.assign({}, process.env, additionalEnvVars);
const child = cp.spawn(command, args, { shell: true, env: env });
child.stdout.on('data', (b: Buffer) => stdoutBuffers.push(b));
child.stderr.on('data', (b: Buffer) => stderrBuffers.push(b));
child.on('error', reject);
child.on('exit', code => {
const stdout = Buffer.concat(stdoutBuffers).toString('utf8').trim();
const stderr = Buffer.concat(stderrBuffers).toString('utf8').trim();
if (stdout) {
Logger.log(loc.stdoutOutput(stdout));
}
if (stderr) {
Logger.log(loc.stderrOutput(stderr));
}
if (code) {
const err = new ExitCodeError(code, stderr);
Logger.log(err.message);
reject(err);
} else {
resolve({ stdout: stdout, stderr: stderr });
}
});
});
}
/**
* Executes a command with admin privileges. The user will be prompted to enter credentials for invocation of
* this function. The exact prompt is platform-dependent.
* @param command The command to execute
* @param args The additional args
*/
export async function executeSudoCommand(command: string): Promise<ProcessOutput> {
return new Promise((resolve, reject) => {
Logger.log(loc.executingCommand(`sudo ${command}`, []));
sudo.exec(command, { name: 'Azure Data Studio' }, (error, stdout, stderr) => {
stdout = stdout?.toString() ?? '';
stderr = stderr?.toString() ?? '';
if (stdout) {
Logger.log(loc.stdoutOutput(stdout));
}
if (stderr) {
Logger.log(loc.stderrOutput(stderr));
}
if (error) {
Logger.log(loc.unexpectedCommandError(error.message));
reject(error);
} else {
resolve({ stdout: stdout, stderr: stderr });
}
});
});
}

View File

@@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as request from 'request';
import * as path from 'path';
import * as loc from '../localizedConstants';
import Logger from './logger';
const DownloadTimeout = 20000;
export namespace HttpClient {
/**
* Downloads a file from the given URL, resolving to the full path of the downloaded file when complete
* @param downloadUrl The URL to download the file from
* @param targetFolder The folder to download the file to
* @returns a promise to a full path to the downloaded file
*/
export function downloadFile(downloadUrl: string, targetFolder: string): Promise<string> {
return download(downloadUrl, targetFolder);
}
/**
* Downloads the text contents of the document at the given URL, resolving to a string containing the text when complete
* @param url The URL of the document whose contents need to be fetched
* @returns a promise to a string that has the contents of document at the provided url
*/
export async function getTextContent(url: string): Promise<string> {
Logger.log(loc.gettingTextContentsOfUrl(url));
return await download(url);
}
/**
* Gets a file/fileContents at the given URL.
* @param downloadUrl The URL to download the file from
* @param targetFolder The folder to download the file to. If not defined then return value is the contents of the downloaded file.
* @returns Full path to the downloaded file or the contents of the file at the given downloadUrl
*/
function download(downloadUrl: string, targetFolder?: string): Promise<string> {
return new Promise((resolve, reject) => {
let totalMegaBytes: number | undefined = undefined;
let receivedBytes = 0;
let printThreshold = 0.1;
let strings: string[] = [];
let downloadRequest = request.get(downloadUrl, { timeout: DownloadTimeout })
.on('error', downloadError => {
Logger.log(loc.downloadError);
Logger.log(downloadError?.message ?? downloadError);
reject(downloadError);
})
.on('response', (response) => {
if (response.statusCode !== 200) {
Logger.log(loc.downloadError);
Logger.log(response.statusMessage);
Logger.log(`response code: ${response.statusCode}`);
return reject(response.statusMessage);
}
if (targetFolder !== undefined) {
const filename = path.basename(response.request.path);
const targetPath = path.join(targetFolder, filename);
Logger.log(loc.downloadingTo(filename, downloadUrl, targetPath));
// Wait to create the WriteStream until here so we can use the actual
// filename based off of the URI.
downloadRequest.pipe(fs.createWriteStream(targetPath))
.on('close', async () => {
Logger.log(loc.downloadFinished);
resolve(targetPath);
})
.on('error', (downloadError) => {
reject(downloadError);
downloadRequest.abort();
});
} else {
response.on('end', () => {
Logger.log(loc.downloadFinished);
resolve(strings.join(''));
});
}
let contentLength = response.headers['content-length'];
let totalBytes = parseInt(contentLength || '0');
totalMegaBytes = totalBytes / (1024 * 1024);
Logger.log(loc.downloadingProgressMb('0', totalMegaBytes.toFixed(2)));
})
.on('data', (data) => {
if (targetFolder === undefined) {
strings.push(data.toString('utf-8'));
}
receivedBytes += data.length;
if (totalMegaBytes) {
let receivedMegaBytes = receivedBytes / (1024 * 1024);
let percentage = receivedMegaBytes / totalMegaBytes;
if (percentage >= printThreshold) {
Logger.log(loc.downloadingProgressMb(receivedMegaBytes.toFixed(2), totalMegaBytes.toFixed(2)));
printThreshold += 0.1;
}
}
});
});
}
}

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* 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 loc from '../localizedConstants';
export class Log {
private _output: vscode.OutputChannel;
constructor() {
this._output = vscode.window.createOutputChannel(loc.azdata);
}
log(msg: string): void {
this._output.appendLine(`[${new Date().toISOString()}] ${msg}`);
}
show(): void {
this._output.show(true);
}
}
const Logger = new Log();
export default Logger;

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Deferred promise
*/
export class Deferred<T = void> {
promise: Promise<T>;
resolve!: (value: T | PromiseLike<T>) => void;
reject!: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult> {
return this.promise.then(onfulfilled, onrejected);
}
}

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 azdataExt from 'azdata-ext';
import * as which from 'which';
import * as loc from '../localizedConstants';
export class NoAzdataError extends Error implements azdataExt.ErrorWithLink {
constructor() {
super(loc.noAzdata);
}
public get messageWithLink(): string {
return loc.noAzdataWithLink;
}
}
/**
* Searches for the first instance of the specified executable in the PATH environment variable
* @param exe The executable to search for
*/
export function searchForCmd(exe: string): Promise<string> {
// Note : This is separated out to allow for easy test stubbing
return new Promise<string>((resolve, reject) => which(exe, (err, path) => err ? reject(err) : resolve(<any>path)));
}
/**
* Gets the message to display for a given error object that may be a variety of types.
* @param error The error object
*/
export function getErrorMessage(error: any): string {
return error.message ?? error;
}