mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
@@ -2,37 +2,61 @@
|
||||
* 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 vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as cp from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as cp from 'promisify-child-process';
|
||||
import * as sudo from 'sudo-prompt';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { OsType } from '../interfaces';
|
||||
import { getErrorMessage } from '../utils';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const extensionOutputChannel = localize('resourceDeployment.outputChannel', "Deployments");
|
||||
const sudoPromptTitle = 'AzureDataStudio';
|
||||
/**
|
||||
* Abstract of platform dependencies
|
||||
*/
|
||||
export interface IPlatformService {
|
||||
osType(): OsType;
|
||||
platform(): string;
|
||||
storagePath(): string;
|
||||
copyFile(source: string, target: string): Promise<void>;
|
||||
fileExists(file: string): Promise<boolean>;
|
||||
openFile(filePath: string): void;
|
||||
showErrorMessage(message: string): void;
|
||||
showErrorMessage(error: Error | string): void;
|
||||
logToOutputChannel(data: string | Buffer, header?: string): void;
|
||||
outputChannelName(): string;
|
||||
showOutputChannel(preserveFocus?: boolean): void;
|
||||
isNotebookNameUsed(title: string): boolean;
|
||||
makeDirectory(path: string): Promise<void>;
|
||||
ensureDirectoryExists(directory: string): Promise<void>;
|
||||
readTextFile(filePath: string): Promise<string>;
|
||||
runCommand(command: string, options?: CommandOptions): Promise<string>;
|
||||
saveTextFile(content: string, path: string): Promise<void>;
|
||||
deleteFile(path: string, ignoreError?: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
interface CommandOutput {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
export interface CommandOptions {
|
||||
workingDirectory?: string;
|
||||
additionalEnvironmentVariables?: NodeJS.ProcessEnv;
|
||||
sudo?: boolean;
|
||||
commandTitle?: string;
|
||||
ignoreError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that provides various services to interact with the platform on which the code runs
|
||||
*/
|
||||
export class PlatformService implements IPlatformService {
|
||||
|
||||
private _outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel(extensionOutputChannel);
|
||||
|
||||
constructor(private _storagePath: string = '') {
|
||||
}
|
||||
|
||||
@@ -44,57 +68,185 @@ export class PlatformService implements IPlatformService {
|
||||
return process.platform;
|
||||
}
|
||||
|
||||
copyFile(source: string, target: string): Promise<void> {
|
||||
return fs.promises.copyFile(source, target);
|
||||
outputChannelName(): string {
|
||||
return this._outputChannel.name;
|
||||
}
|
||||
|
||||
fileExists(file: string): Promise<boolean> {
|
||||
return fs.promises.access(file).then(() => {
|
||||
showOutputChannel(preserveFocus?: boolean): void {
|
||||
this._outputChannel.show(preserveFocus);
|
||||
}
|
||||
|
||||
osType(platform: string = this.platform()): OsType {
|
||||
if (Object.values(OsType).includes(<OsType>platform)) {
|
||||
return <OsType>platform;
|
||||
} else {
|
||||
return OsType.others;
|
||||
}
|
||||
}
|
||||
|
||||
async copyFile(source: string, target: string): Promise<void> {
|
||||
return await fs.promises.copyFile(source, target);
|
||||
}
|
||||
|
||||
async fileExists(file: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(file);
|
||||
return true;
|
||||
}).catch(error => {
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openFile(filePath: string): void {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath));
|
||||
}
|
||||
|
||||
showErrorMessage(message: string): void {
|
||||
vscode.window.showErrorMessage(message);
|
||||
showErrorMessage(error: Error | string): void {
|
||||
vscode.window.showErrorMessage(getErrorMessage(error));
|
||||
}
|
||||
|
||||
isNotebookNameUsed(title: string): boolean {
|
||||
return (azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1);
|
||||
}
|
||||
|
||||
makeDirectory(path: string): Promise<void> {
|
||||
return fs.promises.mkdir(path);
|
||||
async makeDirectory(path: string): Promise<void> {
|
||||
await fs.promises.mkdir(path);
|
||||
}
|
||||
|
||||
readTextFile(filePath: string): Promise<string> {
|
||||
return fs.promises.readFile(filePath, 'utf8');
|
||||
/**
|
||||
*This function ensures that the given {@link directory} does not exist it creates it. It creates only the most leaf folder so if any ancestor folders are missing then this command throws an error.
|
||||
* @param directory - the path to ensure
|
||||
*/
|
||||
async ensureDirectoryExists(directory: string): Promise<void> {
|
||||
if (!await this.fileExists(directory)) {
|
||||
await this.makeDirectory(directory);
|
||||
}
|
||||
}
|
||||
|
||||
runCommand(command: string, options?: CommandOptions): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const env = Object.assign({}, process.env, options && options.additionalEnvironmentVariables);
|
||||
cp.exec(command, {
|
||||
cwd: options && options.workingDirectory,
|
||||
env: env
|
||||
}, (error, stdout, stderror) => {
|
||||
async readTextFile(filePath: string): Promise<string> {
|
||||
return await fs.promises.readFile(filePath, 'utf8');
|
||||
}
|
||||
|
||||
public logToOutputChannel(data: string | Buffer, header?: string): void {
|
||||
//input data is localized by caller
|
||||
data.toString().split(/\r?\n/)
|
||||
.forEach(line => {
|
||||
this._outputChannel.appendLine(header ? header + line : line);
|
||||
});
|
||||
}
|
||||
|
||||
private outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void {
|
||||
data.toString().split(/\r?\n/)
|
||||
.forEach(line => {
|
||||
outputChannel.appendLine(header + line);
|
||||
});
|
||||
}
|
||||
|
||||
async runCommand(command: string, options?: CommandOptions): Promise<string> {
|
||||
if (options && options.commandTitle !== undefined && options.commandTitle !== null) {
|
||||
this._outputChannel.appendLine(`\t[ ${options.commandTitle} ]`); // commandTitle inputs are localized by caller
|
||||
}
|
||||
|
||||
try {
|
||||
if (options && options.sudo) {
|
||||
return await this.runSudoCommand(command, this._outputChannel, options);
|
||||
} else {
|
||||
return await this.runStreamedCommand(command, this._outputChannel, options);
|
||||
}
|
||||
} catch (error) {
|
||||
this._outputChannel.append(localize('platformService.RunCommand.ErroredOut', "\t>>> {0} ... errored out: {1}", command, getErrorMessage(error))); //errors are localized in our code where emitted, other errors are pass through from external components that are not easily localized
|
||||
if (!(options && options.ignoreError)) {
|
||||
throw error;
|
||||
} else {
|
||||
this._outputChannel.append(localize('platformService.RunCommand.IgnoringError', "\t>>> Ignoring error in execution and continuing tool deployment"));
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sudoExec(command: string, options: sudo.SudoOptions): Promise<CommandOutput> {
|
||||
return new Promise<CommandOutput>((resolve, reject) => {
|
||||
sudo.exec(command, options, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(stdout);
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async runSudoCommand(command: string, outputChannel: vscode.OutputChannel, options?: CommandOptions): Promise<string> {
|
||||
outputChannel.appendLine(` sudo> ${command}`);
|
||||
|
||||
if (options && options.workingDirectory) {
|
||||
process.chdir(options.workingDirectory);
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/jorangreef/sudo-prompt/issues/111
|
||||
// DevNote: The environment variable being excluded from getting passed to sudo will never exist on a 'unixy' box. So this affects windows only.
|
||||
// On my testing on windows machine for our usage the environment variables being excluded were not important for the process execution being used here.
|
||||
// If one is trying to use this code elsewhere, one should test on windows thoroughly unless the above issue is fixed.
|
||||
const origEnv: NodeJS.ProcessEnv = Object.assign({}, process.env, options && options.additionalEnvironmentVariables);
|
||||
const env: NodeJS.ProcessEnv = {};
|
||||
|
||||
Object.keys(origEnv).filter(key => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)).forEach((key) => {
|
||||
env[key] = origEnv[key];
|
||||
});
|
||||
// Workaround for https://github.com/jorangreef/sudo-prompt/issues/111 done
|
||||
|
||||
const sudoOptions = {
|
||||
name: sudoPromptTitle,
|
||||
env: env
|
||||
};
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await this.sudoExec(command, sudoOptions);
|
||||
this.outputDataChunk(stdout, outputChannel, localize('platformService.RunCommand.stdout', " stdout: "));
|
||||
this.outputDataChunk(stderr, outputChannel, localize('platformService.RunCommand.stderr', " stderr: "));
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
this.outputDataChunk(error, outputChannel, localize('platformService.RunCommand.stderr', " stderr: "));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async runStreamedCommand(command: string, outputChannel: vscode.OutputChannel, options?: CommandOptions): Promise<string> {
|
||||
const stdoutData: string[] = [];
|
||||
outputChannel.appendLine(` > ${command}`);
|
||||
|
||||
const spawnOptions = {
|
||||
cwd: options && options.workingDirectory,
|
||||
env: Object.assign({}, process.env, options && options.additionalEnvironmentVariables),
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024, // 10 Mb of output can be captured.
|
||||
shell: true,
|
||||
detached: false,
|
||||
windowsHide: true
|
||||
};
|
||||
const child = cp.spawn(command, [], spawnOptions);
|
||||
|
||||
// Add listeners to print stdout and stderr and exit code
|
||||
child.on('exit', (code: number | null, signal: string | null) => {
|
||||
if (code !== null) {
|
||||
outputChannel.appendLine(localize('platformService.RunStreamedCommand.ExitedWithCode', " >>> ${0} ... exited with code: ${1}", command, code));
|
||||
} else {
|
||||
outputChannel.appendLine(localize('platformService.RunStreamedCommand.ExitedWithSignal', " >>> ${0} ... exited with signal: ${1}", command, signal));
|
||||
}
|
||||
});
|
||||
child.stdout.on('data', (data: string | Buffer) => {
|
||||
stdoutData.push(data.toString());
|
||||
this.outputDataChunk(data, outputChannel, localize('platformService.RunCommand.stdout', " stdout: "));
|
||||
});
|
||||
child.stderr.on('data', (data: string | Buffer) => { this.outputDataChunk(data, outputChannel, localize('platformService.RunCommand.stderr', " stderr: ")); });
|
||||
|
||||
await child;
|
||||
return stdoutData.join('');
|
||||
}
|
||||
|
||||
saveTextFile(content: string, path: string): Promise<void> {
|
||||
return fs.promises.writeFile(path, content, 'utf8');
|
||||
}
|
||||
@@ -108,7 +260,7 @@ export class PlatformService implements IPlatformService {
|
||||
}
|
||||
catch (error) {
|
||||
if (ignoreError) {
|
||||
console.error('Error occured deleting file: ', getErrorMessage(error));
|
||||
console.error('Error occurred deleting file: ', getErrorMessage(error));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user