azdata improvements (#11516)

* azdata improvements

* Don't error on sudo command stderr either

* Improve output channel logging for commands

* Fix childprocess stuff

* pr comments

* Fix compile errors

* more pr comments
This commit is contained in:
Charles Gagnon
2020-07-28 08:43:10 -07:00
committed by GitHub
parent 3c7f2df156
commit cf6d02d2b4
12 changed files with 255 additions and 105 deletions

View File

@@ -9,23 +9,25 @@ import * as sudo from 'sudo-prompt';
import * as loc from '../localizedConstants';
/**
* Wrapper error for when an unexpected exit code was recieved
* Wrapper error for when an unexpected exit code was received
*/
export class ExitCodeError extends Error {
constructor(public code: number) {
super(`Unexpected exit code ${code}`);
super(loc.unexpectedExitCode(code));
}
}
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 outputChannel Channel used to display diagnostic information
*/
export async function executeCommand(command: string, args?: string[], outputChannel?: vscode.OutputChannel): Promise<string> {
export async function executeCommand(command: string, args: string[], outputChannel: vscode.OutputChannel): Promise<ProcessOutput> {
return new Promise((resolve, reject) => {
outputChannel?.appendLine(loc.executingCommand(command, args ?? []));
outputChannel.appendLine(loc.executingCommand(command, args));
const stdoutBuffers: Buffer[] = [];
const stderrBuffers: Buffer[] = [];
const child = cp.spawn(command, args, { shell: true });
@@ -33,12 +35,20 @@ export async function executeCommand(command: string, args?: string[], outputCha
child.stderr.on('data', (b: Buffer) => stderrBuffers.push(b));
child.on('error', reject);
child.on('exit', code => {
if (stderrBuffers.length > 0) {
reject(new Error(Buffer.concat(stderrBuffers).toString('utf8').trim()));
} else if (code) {
reject(new ExitCodeError(code));
const stdout = Buffer.concat(stdoutBuffers).toString('utf8').trim();
const stderr = Buffer.concat(stderrBuffers).toString('utf8').trim();
if (stdout) {
outputChannel.appendLine(loc.stdoutOutput(stdout));
}
if (stderr) {
outputChannel.appendLine(loc.stdoutOutput(stderr));
}
if (code) {
const err = new ExitCodeError(code);
outputChannel.appendLine(err.message);
reject(err);
} else {
resolve(Buffer.concat(stdoutBuffers).toString('utf8').trim());
resolve({ stdout: stdout, stderr: stderr });
}
});
});
@@ -51,16 +61,23 @@ export async function executeCommand(command: string, args?: string[], outputCha
* @param args The additional args
* @param outputChannel Channel used to display diagnostic information
*/
export async function executeSudoCommand(command: string, outputChannel?: vscode.OutputChannel): Promise<string> {
return new Promise<string>((resolve, reject) => {
outputChannel?.appendLine(loc.executingCommand(`sudo ${command}`, []));
export async function executeSudoCommand(command: string, outputChannel: vscode.OutputChannel): Promise<ProcessOutput> {
return new Promise((resolve, reject) => {
outputChannel.appendLine(loc.executingCommand(`sudo ${command}`, []));
sudo.exec(command, { name: vscode.env.appName }, (error, stdout, stderr) => {
stdout = stdout?.toString() ?? '';
stderr = stderr?.toString() ?? '';
if (stdout) {
outputChannel.appendLine(loc.stdoutOutput(stdout));
}
if (stderr) {
outputChannel.appendLine(loc.stdoutOutput(stderr));
}
if (error) {
outputChannel.appendLine(loc.unexpectedCommandError(error.message));
reject(error);
} else if (stderr) {
reject(stderr.toString('utf8'));
} else {
resolve(stdout ? stdout.toString('utf8') : '');
resolve({ stdout: stdout, stderr: stderr });
}
});
});

View File

@@ -6,6 +6,7 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as request from 'request';
import * as path from 'path';
import * as loc from '../localizedConstants';
const DownloadTimeout = 20000;
@@ -13,12 +14,13 @@ const DownloadTimeout = 20000;
export namespace HttpClient {
/**
* Downloads a file from the given URL
* 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 targetPath The path to download the file to
* @param targetFolder The folder to download the file to
* @param outputChannel Channel used to display diagnostic information
* @returns Full path to the downloaded file
*/
export function download(downloadUrl: string, targetPath: string, outputChannel: vscode.OutputChannel): Promise<void> {
export function download(downloadUrl: string, targetFolder: string, outputChannel: vscode.OutputChannel): Promise<string> {
return new Promise((resolve, reject) => {
let totalMegaBytes: number | undefined = undefined;
let receivedBytes = 0;
@@ -35,6 +37,20 @@ export namespace HttpClient {
outputChannel.appendLine(response.statusMessage);
return reject(response.statusMessage);
}
const filename = path.basename(response.request.path);
const targetPath = path.join(targetFolder, filename);
outputChannel.appendLine(loc.downloadingTo(filename, 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 () => {
outputChannel.appendLine(loc.downloadFinished);
resolve(targetPath);
})
.on('error', (downloadError) => {
reject(downloadError);
downloadRequest.abort();
});
let contentLength = response.headers['content-length'];
let totalBytes = parseInt(contentLength || '0');
totalMegaBytes = totalBytes / (1024 * 1024);
@@ -51,14 +67,29 @@ export namespace HttpClient {
}
}
});
downloadRequest.pipe(fs.createWriteStream(targetPath))
.on('close', async () => {
outputChannel.appendLine(loc.downloadFinished);
resolve();
})
.on('error', (downloadError) => {
});
}
/**
* Gets the filename for the specified URL - following redirects as needed
* @param url The URL to get the filename of
*/
export async function getFilename(url: string, outputChannel: vscode.OutputChannel): Promise<string> {
outputChannel.appendLine(loc.gettingFilenameOfUrl(url));
return new Promise((resolve, reject) => {
let httpRequest = request.get(url, { timeout: DownloadTimeout })
.on('error', downloadError => {
reject(downloadError);
downloadRequest.abort();
})
.on('response', (response) => {
if (response.statusCode !== 200) {
return reject(response.statusMessage);
}
// We don't want to actually download the file so abort the request now
httpRequest.abort();
const filename = path.basename(response.request.path);
outputChannel.appendLine(loc.gotFilenameOfUrl(response.request.path, filename));
resolve(filename);
});
});
}

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> {
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);
}
}