SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

View File

@@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
const fs = require('fs');
import * as path from 'path';
import {IConfig} from '../languageservice/interfaces';
import * as SharedConstants from '../models/constants';
/*
* Config class handles getting values from config.json.
*/
export default class Config implements IConfig {
private _configJsonContent: any = undefined;
private _extensionConfigSectionName: string = undefined;
private _fromBuild: boolean = undefined;
constructor(extensionConfigSectionName: string, fromBuild?: boolean) {
this._extensionConfigSectionName = extensionConfigSectionName;
this._fromBuild = fromBuild;
}
public get configJsonContent(): any {
if (this._configJsonContent === undefined) {
this._configJsonContent = this.loadConfig();
}
return this._configJsonContent;
}
public getDownloadUrl(): string {
return this.getConfigValue(SharedConstants.downloadUrlConfigKey);
}
public getInstallDirectory(): string {
return this.getConfigValue(SharedConstants.installDirConfigKey);
}
public getExecutableFiles(): string[] {
return this.getConfigValue(SharedConstants.executableFilesConfigKey);
}
public getPackageVersion(): string {
return this.getConfigValue(SharedConstants.versionConfigKey);
}
public getConfigValue(configKey: string): any {
let json = this.configJsonContent;
let toolsConfig = json[SharedConstants.serviceConfigKey];
let configValue: string = undefined;
if (toolsConfig !== undefined) {
configValue = toolsConfig[configKey];
}
return configValue;
}
public getExtensionConfig(key: string, defaultValue?: any): any {
let json = this.configJsonContent;
let extensionConfig = json[this._extensionConfigSectionName];
let configValue = extensionConfig[key];
if (!configValue) {
configValue = defaultValue;
}
return configValue;
}
public getWorkspaceConfig(key: string, defaultValue?: any): any {
let json = this.configJsonContent;
let configValue = json[key];
if (!configValue) {
configValue = defaultValue;
}
return configValue;
}
public loadConfig(): any {
let configContent = undefined;
if (this._fromBuild) {
let remainingPath = '../../../../../extensions/' + this._extensionConfigSectionName + '/client/out/config.json';
configContent = fs.readFileSync(path.join(__dirname, remainingPath));
}
else {
configContent = fs.readFileSync(path.join(__dirname, '../../../../client/out/config.json'));
}
return JSON.parse(configContent);
}
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import Config from './config';
import { workspace, WorkspaceConfiguration } from 'vscode';
import {IConfig} from '../languageservice/interfaces';
import * as Constants from '../models/constants';
/*
* ExtConfig class handles getting values from workspace config or config.json.
*/
export default class ExtConfig implements IConfig {
constructor(private _extensionConfigSectionName: string, private _config?: IConfig,
private _extensionConfig?: WorkspaceConfiguration,
private _workspaceConfig?: WorkspaceConfiguration) {
if (this._config === undefined) {
this._config = new Config(_extensionConfigSectionName);
}
if (this._extensionConfig === undefined) {
this._extensionConfig = workspace.getConfiguration(_extensionConfigSectionName);
}
if (this._workspaceConfig === undefined) {
this._workspaceConfig = workspace.getConfiguration();
}
}
public getDownloadUrl(): string {
return this.getConfigValue(Constants.downloadUrlConfigKey);
}
public getInstallDirectory(): string {
return this.getConfigValue(Constants.installDirConfigKey);
}
public getExecutableFiles(): string[] {
return this.getConfigValue(Constants.executableFilesConfigKey);
}
public getPackageVersion(): string {
return this.getConfigValue(Constants.versionConfigKey);
}
public getConfigValue(configKey: string): any {
let configValue: string = <string>this.getExtensionConfig(`${Constants.serviceConfigKey}.${configKey}`);
if (!configValue) {
configValue = this._config.getConfigValue(configKey);
}
return configValue;
}
public getExtensionConfig(key: string, defaultValue?: any): any {
let configValue = this._extensionConfig.get(key);
if (configValue === undefined) {
configValue = defaultValue;
}
return configValue;
}
public getWorkspaceConfig(key: string, defaultValue?: any): any {
let configValue = this._workspaceConfig.get(key);
if (configValue === undefined) {
configValue = defaultValue;
}
return configValue;
}
public updateWorkspaceConfig(configKey: string, configValue: any) {
this._workspaceConfig.update(configKey, configValue, true);
}
}

View File

@@ -0,0 +1,280 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import vscode = require('vscode');
import {IExtensionConstants} from '../models/contracts/contracts';
export import TextEditor = vscode.TextEditor;
export default class VscodeWrapper {
private _extensionConstants: IExtensionConstants;
/**
* Output channel for logging. Shared among all instances.
*/
private static _outputChannel: vscode.OutputChannel;
/**
* Default constructor.
*/
public constructor(constants: IExtensionConstants) {
this._extensionConstants = constants;
if (typeof VscodeWrapper._outputChannel === 'undefined') {
VscodeWrapper._outputChannel = this.createOutputChannel(this._extensionConstants.outputChannelName);
}
}
/**
* Get the current active text editor
*/
public get activeTextEditor(): vscode.TextEditor {
return vscode.window.activeTextEditor;
}
/**
* get the current textDocument; any that are open?
*/
public get textDocuments(): vscode.TextDocument[] {
return vscode.workspace.textDocuments;
}
/**
* Parse uri
*/
public parseUri(uri: string): vscode.Uri {
return vscode.Uri.parse(uri);
}
/**
* Get the URI string for the current active text editor
*/
public get activeTextEditorUri(): string {
if (typeof vscode.window.activeTextEditor !== 'undefined' &&
typeof vscode.window.activeTextEditor.document !== 'undefined') {
return vscode.window.activeTextEditor.document.uri.toString();
}
return undefined;
}
public get constants(): IExtensionConstants {
return this._extensionConstants;
}
/**
* Create an output channel in vscode.
*/
public createOutputChannel(channelName: string): vscode.OutputChannel {
return vscode.window.createOutputChannel(channelName);
}
/**
* Executes the command denoted by the given command identifier.
*
* When executing an editor command not all types are allowed to
* be passed as arguments. Allowed are the primitive types `string`, `boolean`,
* `number`, `undefined`, and `null`, as well as classes defined in this API.
* There are no restrictions when executing commands that have been contributed
* by extensions.
*
* @param command Identifier of the command to execute.
* @param rest Parameters passed to the command function.
* @return A thenable that resolves to the returned value of the given command. `undefined` when
* the command handler function doesn't return anything.
* @see vscode.commands.executeCommand
*/
public executeCommand<T>(command: string, ...rest: any[]): Thenable<T> {
return vscode.commands.executeCommand<T>(command, ...rest);
}
/**
* Get the configuration for a extensionName; NOT YET IMPLEMENTED
* @param extensionName The string name of the extension to get the configuration for
*/
public getConfiguration(extensionName: string): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration(extensionName);
}
/**
* @return 'true' if the active editor window has a .sql file, false otherwise
*/
public get isEditingSqlFile(): boolean {
let sqlFile = false;
let editor = this.activeTextEditor;
if (editor) {
if (editor.document.languageId === this._extensionConstants.languageId) {
sqlFile = true;
}
}
return sqlFile;
}
/**
* An event that is emitted when a [text document](#TextDocument) is disposed.
*/
public get onDidCloseTextDocument(): vscode.Event<vscode.TextDocument> {
return vscode.workspace.onDidCloseTextDocument;
}
/**
* An event that is emitted when a [text document](#TextDocument) is opened.
*/
public get onDidOpenTextDocument(): vscode.Event<vscode.TextDocument> {
return vscode.workspace.onDidOpenTextDocument;
}
/**
* An event that is emitted when a [text document](#TextDocument) is saved to disk.
*/
public get onDidSaveTextDocument(): vscode.Event<vscode.TextDocument> {
return vscode.workspace.onDidSaveTextDocument;
}
/**
* Opens the denoted document from disk. Will return early if the
* document is already open, otherwise the document is loaded and the
* [open document](#workspace.onDidOpenTextDocument)-event fires.
* The document to open is denoted by the [uri](#Uri). Two schemes are supported:
*
* file: A file on disk, will be rejected if the file does not exist or cannot be loaded, e.g. `file:///Users/frodo/r.ini`.
* untitled: A new file that should be saved on disk, e.g. `untitled:c:\frodo\new.js`. The language will be derived from the file name.
*
* Uris with other schemes will make this method return a rejected promise.
*
* @param uri Identifies the resource to open.
* @return A promise that resolves to a [document](#TextDocument).
* @see vscode.workspace.openTextDocument
*/
public openTextDocument(uri: vscode.Uri): Thenable<vscode.TextDocument> {
return vscode.workspace.openTextDocument(uri);
}
/**
* Helper to log messages to output channel.
*/
public logToOutputChannel(msg: any): void {
let date: Date = new Date();
if (msg instanceof Array) {
msg.forEach(element => {
VscodeWrapper._outputChannel.appendLine('[' + date.toLocaleTimeString() + '] ' + element.toString());
});
} else {
VscodeWrapper._outputChannel.appendLine('[' + date.toLocaleTimeString() + '] ' + msg.toString());
}
}
/**
* Create a vscode.Range object
* @param start The start position for the range
* @param end The end position for the range
*/
public range(start: vscode.Position, end: vscode.Position): vscode.Range {
return new vscode.Range(start, end);
}
/**
* Create a vscode.Position object
* @param line The line for the position
* @param column The column for the position
*/
public position(line: number, column: number): vscode.Position {
return new vscode.Position(line, column);
}
/**
* Create a vscode.Selection object
* @param start The start postion of the selection
* @param end The end position of the selection
*/
public selection(start: vscode.Position, end: vscode.Position): vscode.Selection {
return new vscode.Selection(start, end);
}
/**
* Formats and shows a vscode error message
*/
public showErrorMessage(msg: string, ...items: string[]): Thenable<string> {
return vscode.window.showErrorMessage(this._extensionConstants.extensionName + ': ' + msg, ...items);
}
/**
* Formats and shows a vscode information message
*/
public showInformationMessage(msg: string, ...items: string[]): Thenable<string> {
return vscode.window.showInformationMessage(this._extensionConstants.extensionName + ': ' + msg, ...items);
}
/**
* Shows a selection list.
*
* @param items An array of items, or a promise that resolves to an array of items.
* @param options Configures the behavior of the selection list.
* @return A promise that resolves to the selected item or undefined.
*/
public showQuickPick<T extends vscode.QuickPickItem>(items: T[] | Thenable<T[]>, options?: vscode.QuickPickOptions): Thenable<T> {
return vscode.window.showQuickPick<T>(items, options);
}
/**
* Show the given document in a text editor. A [column](#ViewColumn) can be provided
* to control where the editor is being shown. Might change the [active editor](#window.activeTextEditor).
*
* @param document A text document to be shown.
* @param column A view column in which the editor should be shown. The default is the [one](#ViewColumn.One), other values
* are adjusted to be __Min(column, columnCount + 1)__.
* @param preserveFocus When `true` the editor will not take focus.
* @return A promise that resolves to an [editor](#TextEditor).
*/
public showTextDocument(document: vscode.TextDocument, column?: vscode.ViewColumn, preserveFocus?: boolean): Thenable<vscode.TextEditor> {
return vscode.window.showTextDocument(document, column, preserveFocus);
}
/**
* Formats and shows a vscode warning message
*/
public showWarningMessage(msg: string): Thenable<string> {
return vscode.window.showWarningMessage(this._extensionConstants.extensionName + ': ' + msg );
}
/**
* Returns a array of the text editors currently visible in the window
*/
public get visibleEditors(): vscode.TextEditor[] {
return vscode.window.visibleTextEditors;
}
/**
* Create an URI from a file system path. The [scheme](#Uri.scheme)
* will be `file`.
*
* @param path A file system or UNC path.
* @return A new Uri instance.
* @see vscode.Uri.file
*/
public uriFile(path: string): vscode.Uri {
return vscode.Uri.file(path);
}
/**
* Create an URI from a string. Will throw if the given value is not
* valid.
*
* @param value The string value of an Uri.
* @return A new Uri instance.
* @see vscode.Uri.parse
*/
public uriParse(value: string): vscode.Uri {
return vscode.Uri.parse(value);
}
/**
* The folder that is open in VS Code. `undefined` when no folder
* has been opened.
*
* @readonly
* @see vscode.workspace.rootPath
*/
public get workspaceRootPath(): string {
return vscode.workspace.rootPath;
}
}

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {IDecompressProvider, IPackage} from './interfaces';
import {ILogger} from '../models/interfaces';
const decompress = require('decompress');
export default class DecompressProvider implements IDecompressProvider {
public decompress(pkg: IPackage, logger: ILogger): Promise<void> {
return new Promise<void>((resolve, reject) => {
decompress(pkg.tmpFile.name, pkg.installPath).then(files => {
logger.appendLine(`Done! ${files.length} files unpacked.\n`);
resolve();
}).catch(decompressErr => {
logger.appendLine(`[ERROR] ${decompressErr}`);
reject(decompressErr);
});
});
}
}

View File

@@ -0,0 +1,146 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {IPackage, IStatusView, PackageError, IHttpClient} from './interfaces';
import {ILogger} from '../models/interfaces';
import {parse as parseUrl, Url} from 'url';
import * as https from 'https';
import * as http from 'http';
import {getProxyAgent} from './proxy';
let fs = require('fs');
/*
* Http client class to handle downloading files using http or https urls
*/
export default class HttpClient implements IHttpClient {
/*
* Downloads a file and stores the result in the temp file inside the package object
*/
public downloadFile(urlString: string, pkg: IPackage, logger: ILogger, statusView: IStatusView, proxy?: string, strictSSL?: boolean): Promise<void> {
const url = parseUrl(urlString);
let options = this.getHttpClientOptions(url, proxy, strictSSL);
let clientRequest = url.protocol === 'http:' ? http.request : https.request;
return new Promise<void>((resolve, reject) => {
if (!pkg.tmpFile || pkg.tmpFile.fd === 0) {
return reject(new PackageError('Temporary package file unavailable', pkg));
}
let request = clientRequest(options, response => {
if (response.statusCode === 301 || response.statusCode === 302) {
// Redirect - download from new location
return resolve(this.downloadFile(response.headers.location, pkg, logger, statusView, proxy, strictSSL));
}
if (response.statusCode !== 200) {
// Download failed - print error message
logger.appendLine(`failed (error code '${response.statusCode}')`);
return reject(new PackageError(response.statusCode.toString(), pkg));
}
// If status code is 200
this.handleSuccessfulResponse(pkg, response, logger, statusView).then(_ => {
resolve();
}).catch(err => {
reject(err);
});
});
request.on('error', error => {
// reject(new PackageError(`Request error: ${error.code || 'NONE'}`, pkg, error));
reject(new PackageError(`Request error: ${error.name || 'NONE'}`, pkg, error));
});
// Execute the request
request.end();
});
}
private getHttpClientOptions(url: Url, proxy?: string, strictSSL?: boolean): any {
const agent = getProxyAgent(url, proxy, strictSSL);
let options: http.RequestOptions = {
host: url.hostname,
path: url.path,
agent: agent,
port: +url.port
};
if (url.protocol === 'https:') {
let httpsOptions: https.RequestOptions = {
host: url.hostname,
path: url.path,
agent: agent,
port: +url.port
};
options = httpsOptions;
}
return options;
}
/*
* Calculate the download percentage and stores in the progress object
*/
public handleDataReceivedEvent(progress: IDownloadProgress, data: any, logger: ILogger, statusView: IStatusView): void {
progress.downloadedBytes += data.length;
// Update status bar item with percentage
if (progress.packageSize > 0) {
let newPercentage = Math.ceil(100 * (progress.downloadedBytes / progress.packageSize));
if (newPercentage !== progress.downloadPercentage) {
statusView.updateServiceDownloadingProgress(progress.downloadPercentage);
progress.downloadPercentage = newPercentage;
}
// Update dots after package name in output console
let newDots = Math.ceil(progress.downloadPercentage / 5);
if (newDots > progress.dots) {
logger.append('.'.repeat(newDots - progress.dots));
progress.dots = newDots;
}
}
return;
}
private handleSuccessfulResponse(pkg: IPackage, response: http.IncomingMessage, logger: ILogger, statusView: IStatusView): Promise<void> {
return new Promise<void>((resolve, reject) => {
let progress: IDownloadProgress = {
packageSize: parseInt(response.headers['content-length'], 10),
dots: 0,
downloadedBytes: 0,
downloadPercentage: 0
};
logger.append(`(${Math.ceil(progress.packageSize / 1024)} KB) `);
response.on('data', data => {
this.handleDataReceivedEvent(progress, data, logger, statusView);
});
let tmpFile = fs.createWriteStream(undefined, { fd: pkg.tmpFile.fd });
response.on('end', () => {
resolve();
});
response.on('error', err => {
reject(new PackageError(`Response error: ${err.name || 'NONE'}`, pkg, err));
});
// Begin piping data from the response to the package file
response.pipe(tmpFile, { end: false });
});
}
}
/*
* Interface to store the values needed to calculate download percentage
*/
export interface IDownloadProgress {
packageSize: number;
downloadedBytes: number;
downloadPercentage: number;
dots: number;
}

View File

@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as tmp from 'tmp';
import {ILogger} from '../models/interfaces';
export interface IStatusView {
installingService(): void;
serviceInstalled(): void;
serviceInstallationFailed(): void;
updateServiceDownloadingProgress(downloadPercentage: number): void;
}
export interface IConfig {
getDownloadUrl(): string;
getInstallDirectory(): string;
getExecutableFiles(): string[];
getPackageVersion(): string;
getExtensionConfig(key: string, defaultValue?: any): any;
getWorkspaceConfig(key: string, defaultValue?: any): any;
getConfigValue(configKey: string): any;
}
export interface IPackage {
url: string;
installPath?: string;
tmpFile: tmp.SynchronousResult;
}
export class PackageError extends Error {
// Do not put PII (personally identifiable information) in the 'message' field as it will be logged to telemetry
constructor(public message: string,
public pkg: IPackage = undefined,
public innerError: any = undefined) {
super(message);
}
}
export interface IHttpClient {
downloadFile(urlString: string, pkg: IPackage, logger: ILogger, statusView: IStatusView, proxy: string, strictSSL: boolean): Promise<void>;
}
export interface IDecompressProvider {
decompress(pkg: IPackage, logger: ILogger): Promise<void>;
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Url, parse as parseUrl } from 'url';
let HttpProxyAgent = require('http-proxy-agent');
let HttpsProxyAgent = require('https-proxy-agent');
function getSystemProxyURL(requestURL: Url): string {
if (requestURL.protocol === 'http:') {
return process.env.HTTP_PROXY || process.env.http_proxy || undefined;
} else if (requestURL.protocol === 'https:') {
return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || undefined;
}
return undefined;
}
/*
* Returns the proxy agent using the proxy url in the parameters or the system proxy. Returns null if no proxy found
*/
export function getProxyAgent(requestURL: Url, proxy?: string, strictSSL?: boolean): any {
const proxyURL = proxy || getSystemProxyURL(requestURL);
if (!proxyURL) {
return undefined;
}
const proxyEndpoint = parseUrl(proxyURL);
if (!/^https?:$/.test(proxyEndpoint.protocol)) {
return undefined;
}
strictSSL = strictSSL || true;
const opts = {
host: proxyEndpoint.hostname,
port: Number(proxyEndpoint.port),
auth: proxyEndpoint.auth,
rejectUnauthorized: strictSSL
};
return requestURL.protocol === 'http:' ? new HttpProxyAgent(opts) : new HttpsProxyAgent(opts);
}

View File

@@ -0,0 +1,112 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as path from 'path';
import {Runtime} from '../models/platform';
import ServiceDownloadProvider from './serviceDownloadProvider';
import {IConfig, IStatusView} from './interfaces';
let fs = require('fs-extra-promise');
/*
* Service Provider class finds the SQL tools service executable file or downloads it if doesn't exist.
*/
export default class ServerProvider {
constructor(private _downloadProvider: ServiceDownloadProvider,
private _config: IConfig,
private _statusView: IStatusView,
private _extensionConfigSectionName: string) {
}
/**
* Public get method for downloadProvider
*/
public get downloadProvider(): ServiceDownloadProvider {
return this._downloadProvider;
}
/**
* Given a file path, returns the path to the SQL Tools service file.
*/
public findServerPath(filePath: string, executableFiles: string[] = undefined): Promise<string> {
return fs.lstatAsync(filePath).then(stats => {
// If a file path was passed, assume its the launch file.
if (stats.isFile()) {
return filePath;
}
// Otherwise, search the specified folder.
let candidate: string;
if (executableFiles === undefined && this._config !== undefined) {
executableFiles = this._config.getExecutableFiles();
}
if (executableFiles !== undefined) {
executableFiles.forEach(element => {
let executableFile = path.join(filePath, element);
if (candidate === undefined && fs.existsSync(executableFile)) {
candidate = executableFile;
return candidate;
}
});
}
return candidate;
});
}
/**
* Download the service if doesn't exist and returns the file path.
*/
public getOrDownloadServer(runtime: Runtime): Promise<string> {
// Attempt to find launch file path first from options, and then from the default install location.
// If SQL tools service can't be found, download it.
return new Promise<string>((resolve, reject) => {
return this.getServerPath(runtime).then(result => {
if (result === undefined) {
return this.downloadServerFiles(runtime).then ( downloadResult => {
resolve(downloadResult);
});
} else {
return resolve(result);
}
}).catch(err => {
return reject(err);
});
}).catch(err => {
throw err;
});
}
/**
* Returns the path of the installed service
*/
public getServerPath(runtime: Runtime): Promise<string> {
const installDirectory = this._downloadProvider.getInstallDirectory(runtime, this._extensionConfigSectionName);
return this.findServerPath(installDirectory);
}
/**
* Downloads the service and returns the path of the installed service
*/
public downloadServerFiles(runtime: Runtime): Promise<string> {
return new Promise<string>((resolve, reject) => {
const installDirectory = this._downloadProvider.getInstallDirectory(runtime, this._extensionConfigSectionName);
return this._downloadProvider.installService(runtime).then( _ => {
return this.findServerPath(installDirectory).then ( result => {
return resolve(result);
});
}).catch(err => {
this._statusView.serviceInstallationFailed();
reject(err);
});
});
}
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {IStatusView} from './interfaces';
import vscode = require('vscode');
import {IExtensionConstants} from '../models/contracts/contracts';
import * as Constants from '../models/constants';
/*
* The status class which includes the service initialization result.
*/
export class ServerInitializationResult {
public constructor(
public installedBeforeInitializing: Boolean = false,
public isRunning: Boolean = false,
public serverPath: string = undefined
) {
}
public Clone(): ServerInitializationResult {
return new ServerInitializationResult(this.installedBeforeInitializing, this.isRunning, this.serverPath);
}
public WithRunning(isRunning: Boolean): ServerInitializationResult {
return new ServerInitializationResult(this.installedBeforeInitializing, isRunning, this.serverPath);
}
}
/*
* The status class shows service installing progress in UI
*/
export class ServerStatusView implements IStatusView, vscode.Disposable {
private _numberOfSecondsBeforeHidingMessage = 5000;
private _statusBarItem: vscode.StatusBarItem = undefined;
private _progressTimerId: NodeJS.Timer;
private _constants: IExtensionConstants;
constructor(constants: IExtensionConstants) {
this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
vscode.window.onDidChangeActiveTextEditor((params) => this.onDidChangeActiveTextEditor(params));
vscode.workspace.onDidCloseTextDocument((params) => this.onDidCloseTextDocument(params));
this._constants = constants;
}
public installingService(): void {
this._statusBarItem.command = undefined;
this._statusBarItem.show();
this.showProgress('$(desktop-download) ' + Constants.serviceInstalling);
}
public updateServiceDownloadingProgress(downloadPercentage: number): void {
this._statusBarItem.text = '$(cloud-download) ' + `${Constants.serviceDownloading} ... ${downloadPercentage}%`;
this._statusBarItem.show();
}
public serviceInstalled(): void {
this._statusBarItem.command = undefined;
this._statusBarItem.text = this._constants.serviceInstalled;
this._statusBarItem.show();
// Cleat the status bar after 2 seconds
setTimeout(() => {
this._statusBarItem.hide();
}, this._numberOfSecondsBeforeHidingMessage);
}
public serviceInstallationFailed(): void {
this._statusBarItem.command = undefined;
this._statusBarItem.text = this._constants.serviceInstallationFailed;
this._statusBarItem.show();
}
private showProgress(statusText: string): void {
let index = 0;
let progressTicks = [ '|', '/', '-', '\\'];
this._progressTimerId = setInterval(() => {
index++;
if (index > 3) {
index = 0;
}
let progressTick = progressTicks[index];
if (this._statusBarItem.text !== this._constants.serviceInstalled) {
this._statusBarItem.text = statusText + ' ' + progressTick;
this._statusBarItem.show();
}
}, 200);
}
dispose(): void {
this.destroyStatusBar();
}
private hideLastShownStatusBar(): void {
if (typeof this._statusBarItem !== 'undefined') {
this._statusBarItem.hide();
}
}
private onDidChangeActiveTextEditor(editor: vscode.TextEditor): void {
// Hide the most recently shown status bar
this.hideLastShownStatusBar();
}
private onDidCloseTextDocument(doc: vscode.TextDocument): void {
// Remove the status bar associated with the document
this.destroyStatusBar();
}
private destroyStatusBar(): void {
if (typeof this._statusBarItem !== 'undefined') {
this._statusBarItem.dispose();
}
}
}

View File

@@ -0,0 +1,492 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
'use strict';
import { ExtensionContext, workspace, window, OutputChannel, languages } from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions,
TransportKind, RequestType, NotificationType, NotificationHandler,
ErrorAction, CloseAction } from 'dataprotocol-client';
import VscodeWrapper from '../controllers/vscodeWrapper';
import Telemetry from '../models/telemetry';
import * as Utils from '../models/utils';
import {VersionRequest, IExtensionConstants} from '../models/contracts/contracts';
import {Logger} from '../models/logger';
import Constants = require('../models/constants');
import {ILanguageClientHelper} from '../models/contracts/languageService';
import ServerProvider from './server';
import ServiceDownloadProvider from './serviceDownloadProvider';
import DecompressProvider from './decompressProvider';
import HttpClient from './httpClient';
import ExtConfig from '../configurations/extConfig';
import {PlatformInformation, Runtime} from '../models/platform';
import {ServerInitializationResult, ServerStatusView} from './serverStatus';
import StatusView from '../views/statusView';
import * as LanguageServiceContracts from '../models/contracts/languageService';
import * as SharedConstants from '../models/constants';
import * as utils from '../models/utils';
var path = require('path');
import ServiceStatus from './serviceStatus';
let opener = require('opener');
let _channel: OutputChannel = undefined;
const fs = require('fs-extra');
/**
* @interface IMessage
*/
interface IMessage {
jsonrpc: string;
}
/**
* Handle Language Service client errors
* @class LanguageClientErrorHandler
*/
class LanguageClientErrorHandler {
private vscodeWrapper: VscodeWrapper;
/**
* Creates an instance of LanguageClientErrorHandler.
* @memberOf LanguageClientErrorHandler
*/
constructor(constants: IExtensionConstants) {
if (!this.vscodeWrapper) {
this.vscodeWrapper = new VscodeWrapper(constants);
}
Telemetry.getRuntimeId = this.vscodeWrapper.constants.getRuntimeId;
}
/**
* Show an error message prompt with a link to known issues wiki page
* @memberOf LanguageClientErrorHandler
*/
showOnErrorPrompt(): void {
let extensionConstants = this.vscodeWrapper.constants;
Telemetry.sendTelemetryEvent(extensionConstants.serviceName + 'Crash');
this.vscodeWrapper.showErrorMessage(
extensionConstants.serviceCrashMessage,
SharedConstants.serviceCrashButton).then(action => {
if (action && action === SharedConstants.serviceCrashButton) {
opener(extensionConstants.serviceCrashLink);
}
});
}
/**
* Callback for language service client error
*
* @param {Error} error
* @param {Message} message
* @param {number} count
* @returns {ErrorAction}
*
* @memberOf LanguageClientErrorHandler
*/
error(error: Error, message: IMessage, count: number): ErrorAction {
this.showOnErrorPrompt();
// we don't retry running the service since crashes leave the extension
// in a bad, unrecovered state
return ErrorAction.Shutdown;
}
/**
* Callback for language service client closed
*
* @returns {CloseAction}
*
* @memberOf LanguageClientErrorHandler
*/
closed(): CloseAction {
this.showOnErrorPrompt();
// we don't retry running the service since crashes leave the extension
// in a bad, unrecovered state
return CloseAction.DoNotRestart;
}
}
// The Service Client class handles communication with the VS Code LanguageClient
export default class SqlToolsServiceClient {
// singleton instance
private static _instance: SqlToolsServiceClient = undefined;
private static _constants: IExtensionConstants = undefined;
public static get constants(): IExtensionConstants {
return this._constants;
}
public static set constants(constantsObject: IExtensionConstants) {
this._constants = constantsObject;
Telemetry.getRuntimeId = this._constants.getRuntimeId;
}
private static _helper: ILanguageClientHelper = undefined;
public static get helper(): ILanguageClientHelper {
return this._helper;
}
public static set helper(helperObject: ILanguageClientHelper) {
this._helper = helperObject;
}
// VS Code Language Client
private _client: LanguageClient = undefined;
// getter method for the Language Client
private get client(): LanguageClient {
return this._client;
}
private set client(client: LanguageClient) {
this._client = client;
}
public installDirectory: string;
private _downloadProvider: ServiceDownloadProvider;
private _vscodeWrapper: VscodeWrapper;
private _serviceStatus: ServiceStatus;
private _languageClientStartTime: number = undefined;
private _installationTime: number = undefined;
constructor(
private _server: ServerProvider,
private _logger: Logger,
private _statusView: StatusView,
private _config: ExtConfig) {
this._downloadProvider = _server.downloadProvider;
if (!this._vscodeWrapper) {
this._vscodeWrapper = new VscodeWrapper(SqlToolsServiceClient.constants);
}
this._serviceStatus = new ServiceStatus(SqlToolsServiceClient._constants.serviceName);
}
// gets or creates the singleton service client instance
public static get instance(): SqlToolsServiceClient {
if (this._instance === undefined) {
let constants = this._constants;
let config = new ExtConfig(constants.extensionConfigSectionName);
_channel = window.createOutputChannel(constants.serviceInitializingOutputChannelName);
let logger = new Logger(text => _channel.append(text), constants);
let serverStatusView = new ServerStatusView(constants);
let httpClient = new HttpClient();
let decompressProvider = new DecompressProvider();
let downloadProvider = new ServiceDownloadProvider(config, logger, serverStatusView, httpClient,
decompressProvider, constants, false);
let serviceProvider = new ServerProvider(downloadProvider, config, serverStatusView, constants.extensionConfigSectionName);
let statusView = new StatusView();
this._instance = new SqlToolsServiceClient(serviceProvider, logger, statusView, config);
}
return this._instance;
}
// initialize the Service Client instance by launching
// out-of-proc server through the LanguageClient
public initialize(context: ExtensionContext): Promise<any> {
this._logger.appendLine(SqlToolsServiceClient._constants.serviceInitializing);
this._languageClientStartTime = Date.now();
return PlatformInformation.getCurrent(SqlToolsServiceClient._constants.getRuntimeId, SqlToolsServiceClient._constants.extensionName).then(platformInfo => {
return this.initializeForPlatform(platformInfo, context);
}).catch(err => {
this._vscodeWrapper.showErrorMessage(err)
});
}
public initializeForPlatform(platformInfo: PlatformInformation, context: ExtensionContext): Promise<ServerInitializationResult> {
return new Promise<ServerInitializationResult>( (resolve, reject) => {
this._logger.appendLine(SqlToolsServiceClient._constants.commandsNotAvailableWhileInstallingTheService);
this._logger.appendLine();
this._logger.append(`Platform: ${platformInfo.toString()}`);
if (!platformInfo.isValidRuntime()) {
// if it's an unknown Linux distro then try generic Linux x64 and give a warning to the user
if (platformInfo.isLinux()) {
this._logger.appendLine(Constants.usingDefaultPlatformMessage);
platformInfo.runtimeId = Runtime.Linux_64;
}
let ignoreWarning: boolean = this._config.getWorkspaceConfig(Constants.ignorePlatformWarning, false);
if (!ignoreWarning) {
this._vscodeWrapper.showErrorMessage(
Constants.unsupportedPlatformErrorMessage,
Constants.neverShowAgain)
.then(action => {
if (action === Constants.neverShowAgain) {
this._config.updateWorkspaceConfig(Constants.ignorePlatformWarning, true);
}
});
}
Telemetry.sendTelemetryEvent('UnsupportedPlatform', {platform: platformInfo.toString()} );
}
if (platformInfo.runtimeId) {
this._logger.appendLine(` (${platformInfo.getRuntimeDisplayName()})`);
} else {
this._logger.appendLine();
}
this._logger.appendLine();
this._server.getServerPath(platformInfo.runtimeId).then(serverPath => {
if (serverPath === undefined) {
// Check if the service already installed and if not open the output channel to show the logs
if (_channel !== undefined) {
_channel.show();
}
let installationStartTime = Date.now();
this._server.downloadServerFiles(platformInfo.runtimeId).then ( installedServerPath => {
this._installationTime = Date.now() - installationStartTime;
this.initializeLanguageClient(installedServerPath, context, platformInfo.runtimeId);
resolve(new ServerInitializationResult(true, true, installedServerPath));
}).catch(downloadErr => {
reject(downloadErr);
});
} else {
this.initializeLanguageClient(serverPath, context, platformInfo.runtimeId);
resolve(new ServerInitializationResult(false, true, serverPath));
}
}).catch(err => {
Utils.logDebug(SqlToolsServiceClient._constants.serviceLoadingFailed + ' ' + err, SqlToolsServiceClient._constants.extensionConfigSectionName);
Utils.showErrorMsg(SqlToolsServiceClient._constants.serviceLoadingFailed, SqlToolsServiceClient._constants.extensionName);
Telemetry.sendTelemetryEvent('ServiceInitializingFailed');
reject(err);
});
});
}
/**
* Initializes the SQL language configuration
*
* @memberOf SqlToolsServiceClient
*/
private initializeLanguageConfiguration(): void {
languages.setLanguageConfiguration('sql', {
comments: {
lineComment: '--',
blockComment: ['/*', '*/']
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
__characterPairSupport: {
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string'] },
{ open: '\'', close: '\'', notIn: ['string', 'comment'] }
]
}
});
}
private initializeLanguageClient(serverPath: string, context: ExtensionContext, runtimeId: Runtime): void {
if (serverPath === undefined) {
Utils.logDebug(SqlToolsServiceClient._constants.invalidServiceFilePath, SqlToolsServiceClient._constants.extensionConfigSectionName);
throw new Error(SqlToolsServiceClient._constants.invalidServiceFilePath);
} else {
let self = this;
if (SqlToolsServiceClient._constants.languageId === 'sql') {
self.initializeLanguageConfiguration();
}
// Use default createServerOptions if one isn't specified
let serverOptions: ServerOptions = SqlToolsServiceClient._helper ?
SqlToolsServiceClient._helper.createServerOptions(serverPath, runtimeId) : self.createServerOptions(serverPath);
this.client = this.createLanguageClient(serverOptions);
this.installDirectory = this._downloadProvider.getInstallDirectory(runtimeId, SqlToolsServiceClient._constants.extensionConfigSectionName);
if (context !== undefined) {
// Create the language client and start the client.
let disposable = this.client.start();
// Push the disposable to the context's subscriptions so that the
// client can be deactivated on extension deactivation
context.subscriptions.push(disposable);
}
}
}
public createClient(context: ExtensionContext, runtimeId: Runtime, languageClientHelper: ILanguageClientHelper, executableFiles: string[]): Promise<LanguageClient> {
return new Promise<LanguageClient>( (resolve, reject) => {
let client: LanguageClient;
this._server.findServerPath(this.installDirectory, executableFiles).then(serverPath => {
if (serverPath === undefined) {
reject(new Error(SqlToolsServiceClient._constants.invalidServiceFilePath));
} else {
let serverOptions: ServerOptions = languageClientHelper ?
languageClientHelper.createServerOptions(serverPath, runtimeId) : this.createServerOptions(serverPath);
// Options to control the language client
let clientOptions: LanguageClientOptions = {
documentSelector: [SqlToolsServiceClient._constants.languageId],
providerId: '',
synchronize: {
configurationSection: SqlToolsServiceClient._constants.extensionConfigSectionName
},
errorHandler: new LanguageClientErrorHandler(SqlToolsServiceClient._constants),
serverConnectionMetadata: this._config.getConfigValue(Constants.serverConnectionMetadata)
};
this._serviceStatus.showServiceLoading();
// cache the client instance for later use
client = new LanguageClient(SqlToolsServiceClient._constants.serviceName, serverOptions, clientOptions);
if (context !== undefined) {
// Create the language client and start the client.
let disposable = client.start();
// Push the disposable to the context's subscriptions so that the
// client can be deactivated on extension deactivation
context.subscriptions.push(disposable);
}
client.onReady().then(this._serviceStatus.showServiceLoaded);
resolve(client);
}
}, error => {
reject(error);
});
});
}
private createServerOptions(servicePath): ServerOptions {
let serverArgs = [];
let serverCommand: string = servicePath;
if (servicePath.endsWith('.dll')) {
serverArgs = [servicePath];
serverCommand = 'dotnet';
}
// Enable diagnostic logging in the service if it is configured
let config = workspace.getConfiguration(SqlToolsServiceClient._constants.extensionConfigSectionName);
if (config) {
let logDebugInfo = config[Constants.configLogDebugInfo];
if (logDebugInfo) {
serverArgs.push('--enable-logging');
}
}
serverArgs.push('--log-dir');
let logFileLocation = path.join(utils.getDefaultLogLocation(), SqlToolsServiceClient.constants.extensionName);
serverArgs.push(logFileLocation);
// run the service host using dotnet.exe from the path
let serverOptions: ServerOptions = { command: serverCommand, args: serverArgs, transport: TransportKind.stdio };
return serverOptions;
}
private createLanguageClient(serverOptions: ServerOptions): LanguageClient {
// Options to control the language client
let clientOptions: LanguageClientOptions = {
documentSelector: [SqlToolsServiceClient._constants.languageId],
providerId: SqlToolsServiceClient._constants.providerId,
synchronize: {
configurationSection: SqlToolsServiceClient._constants.extensionConfigSectionName
},
errorHandler: new LanguageClientErrorHandler(SqlToolsServiceClient._constants),
serverConnectionMetadata: this._config.getConfigValue(Constants.serverConnectionMetadata)
};
this._serviceStatus.showServiceLoading();
// cache the client instance for later use
let client = new LanguageClient(SqlToolsServiceClient._constants.serviceName, serverOptions, clientOptions);
client.onReady().then( () => {
this.checkServiceCompatibility();
this._serviceStatus.showServiceLoaded();
client.onNotification(LanguageServiceContracts.TelemetryNotification.type, this.handleLanguageServiceTelemetryNotification());
client.onNotification(LanguageServiceContracts.StatusChangedNotification.type, this.handleLanguageServiceStatusNotification());
// Report the language client startup time
let endTime = Date.now();
let installationTime = this._installationTime || 0;
let totalTime = endTime - this._languageClientStartTime;
let processStartupTime = totalTime - installationTime;
Telemetry.sendTelemetryEvent('startup/LanguageClientStarted', {
installationTime: String(installationTime),
processStartupTime: String(processStartupTime),
totalTime: String(totalTime),
beginningTimestamp: String(this._languageClientStartTime)
});
this._languageClientStartTime = undefined;
this._installationTime = undefined;
});
return client;
}
private handleLanguageServiceTelemetryNotification(): NotificationHandler<LanguageServiceContracts.TelemetryParams> {
return (event: LanguageServiceContracts.TelemetryParams): void => {
Telemetry.sendTelemetryEvent(event.params.eventName, event.params.properties, event.params.measures);
};
}
/**
* Public for testing purposes only.
*/
public handleLanguageServiceStatusNotification(): NotificationHandler<LanguageServiceContracts.StatusChangeParams> {
return (event: LanguageServiceContracts.StatusChangeParams): void => {
this._statusView.languageServiceStatusChanged(event.ownerUri, event.status);
};
}
/**
* Send a request to the service client
* @param type The of the request to make
* @param params The params to pass with the request
* @returns A thenable object for when the request receives a response
*/
public sendRequest<P, R, E>(type: RequestType<P, R, E>, params?: P, client: LanguageClient = undefined): Thenable<R> {
if (client === undefined) {
client = this._client;
}
if (client !== undefined) {
return client.sendRequest(type, params);
}
}
/**
* Register a handler for a notification type
* @param type The notification type to register the handler for
* @param handler The handler to register
*/
public onNotification<P>(type: NotificationType<P>, handler: NotificationHandler<P>, client: LanguageClient = undefined): void {
if (client === undefined) {
client = this._client;
}
if (client !== undefined) {
return client.onNotification(type, handler);
}
}
public checkServiceCompatibility(): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
this._client.sendRequest(VersionRequest.type, undefined).then((result) => {
Utils.logDebug(SqlToolsServiceClient._constants.extensionName + ' service client version: ' + result, SqlToolsServiceClient._constants.extensionConfigSectionName);
if (result === undefined || !result.startsWith(SqlToolsServiceClient._constants.serviceCompatibleVersion)) {
Utils.showErrorMsg(Constants.serviceNotCompatibleError, SqlToolsServiceClient._constants.extensionName);
Utils.logDebug(Constants.serviceNotCompatibleError, SqlToolsServiceClient._constants.extensionConfigSectionName);
resolve(false);
} else {
resolve(true);
}
});
});
}
}

View File

@@ -0,0 +1,187 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Runtime, getRuntimeDisplayName } from '../models/platform';
import * as path from 'path';
import { IConfig, IStatusView, IPackage, PackageError, IHttpClient, IDecompressProvider } from './interfaces';
import { ILogger } from '../models/interfaces';
import Constants = require('../models/constants');
import * as tmp from 'tmp';
import {IExtensionConstants} from '../models/contracts/contracts';
let fse = require('fs-extra');
/*
* Service Download Provider class which handles downloading the SQL Tools service.
*/
export default class ServiceDownloadProvider {
constructor(private _config: IConfig,
private _logger: ILogger,
private _statusView: IStatusView,
private _httpClient: IHttpClient,
private _decompressProvider: IDecompressProvider,
private _extensionConstants: IExtensionConstants,
private _fromBuild: boolean) {
// Ensure our temp files get cleaned up in case of error.
tmp.setGracefulCleanup();
}
/**
* Returns the download url for given platform
*/
public getDownloadFileName(platform: Runtime): string {
let fileNamesJson = this._config.getConfigValue('downloadFileNames');
console.info('Platform: ', platform.toString());
let fileName = fileNamesJson[platform.toString()];
console.info('Filename: ', fileName);
if (fileName === undefined) {
if (process.platform === 'linux') {
throw new Error('Unsupported linux distribution');
} else {
throw new Error(`Unsupported platform: ${process.platform}`);
}
}
return fileName;
}
/**
* Returns SQL tools service installed folder.
*/
public getInstallDirectory(platform: Runtime, extensionConfigSectionName: string): string {
let basePath = this.getInstallDirectoryRoot(platform, extensionConfigSectionName);
let versionFromConfig = this._config.getPackageVersion();
basePath = basePath.replace('{#version#}', versionFromConfig);
basePath = basePath.replace('{#platform#}', getRuntimeDisplayName(platform));
if (!fse.existsSync(basePath)) {
fse.mkdirsSync(basePath);
}
return basePath;
}
private getLocalUserFolderPath(platform: Runtime): string {
if (platform) {
switch (platform) {
case Runtime.Windows_64:
case Runtime.Windows_86:
return process.env.APPDATA;
case Runtime.OSX:
return process.env.HOME + '/Library/Preferences';
default:
return process.env.HOME;
}
}
}
/**
* Returns SQL tools service installed folder root.
*/
public getInstallDirectoryRoot(platform: Runtime, extensionConfigSectionName: string): string {
let installDirFromConfig : string;
installDirFromConfig = this._config.getInstallDirectory();
if (!installDirFromConfig || installDirFromConfig === '') {
let rootFolderName: string = '.sqlops';
if (platform === Runtime.Windows_64 || platform === Runtime.Windows_86) {
rootFolderName = 'sqlops';
}
installDirFromConfig = path.join(this.getLocalUserFolderPath(platform), `/${rootFolderName}/${this._extensionConstants.installFolderName}/{#version#}/{#platform#}`);
}
let basePath: string;
if (path.isAbsolute(installDirFromConfig)) {
basePath = installDirFromConfig;
} else if (this._fromBuild) {
basePath = path.join(__dirname, '../../../../../extensions/' + extensionConfigSectionName + '/' + installDirFromConfig);
}
else {
// The path from config is relative to the out folder
basePath = path.join(__dirname, '../../../../' + installDirFromConfig);
}
return basePath;
}
private getGetDownloadUrl(fileName: string): string {
let baseDownloadUrl = this._config.getDownloadUrl();
let version = this._config.getPackageVersion();
baseDownloadUrl = baseDownloadUrl.replace('{#version#}', version);
baseDownloadUrl = baseDownloadUrl.replace('{#fileName#}', fileName);
return baseDownloadUrl;
}
/**
* Downloads the service and decompress it in the install folder.
*/
public installService(platform: Runtime): Promise<boolean> {
const proxy = <string>this._config.getWorkspaceConfig('http.proxy');
const strictSSL = this._config.getWorkspaceConfig('http.proxyStrictSSL', true);
return new Promise<boolean>((resolve, reject) => {
const fileName = this.getDownloadFileName(platform);
const installDirectory = this.getInstallDirectory(platform, this._extensionConstants.extensionConfigSectionName);
this._logger.appendLine(`${this._extensionConstants.serviceInstallingTo} ${installDirectory}.`);
const urlString = this.getGetDownloadUrl(fileName);
this._logger.appendLine(`${Constants.serviceDownloading} ${urlString}`);
let pkg: IPackage = {
installPath: installDirectory,
url: urlString,
tmpFile: undefined
};
this.createTempFile(pkg).then(tmpResult => {
pkg.tmpFile = tmpResult;
this._httpClient.downloadFile(pkg.url, pkg, this._logger, this._statusView, proxy, strictSSL).then(_ => {
this._logger.logDebug(`Downloaded to ${pkg.tmpFile.name}...`);
this._logger.appendLine(' Done!');
this.install(pkg).then(result => {
resolve(true);
}).catch(installError => {
reject(installError);
});
}).catch(downloadError => {
this._logger.appendLine(`[ERROR] ${downloadError}`);
reject(downloadError);
});
});
});
}
private createTempFile(pkg: IPackage): Promise<tmp.SynchronousResult> {
return new Promise<tmp.SynchronousResult>((resolve, reject) => {
tmp.file({ prefix: 'package-' }, (err, path, fd, cleanupCallback) => {
if (err) {
return reject(new PackageError('Error from tmp.file', pkg, err));
}
resolve(<tmp.SynchronousResult>{ name: path, fd: fd, removeCallback: cleanupCallback });
});
});
}
private install(pkg: IPackage): Promise<void> {
this._logger.appendLine('Installing ...');
this._statusView.installingService();
return new Promise<void>((resolve, reject) => {
this._decompressProvider.decompress(pkg, this._logger).then(_ => {
this._statusView.serviceInstalled();
resolve();
}).catch(err => {
reject(err);
});
});
}
}

View File

@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Runtime, PlatformInformation } from '../models/platform';
import Config from '../configurations/config';
import ServiceDownloadProvider from './serviceDownloadProvider';
import DecompressProvider from './decompressProvider';
import HttpClient from './httpClient';
import ServerProvider from './server';
import { IStatusView } from './interfaces';
import { ILogger } from '../models/interfaces';
import { IExtensionConstants } from '../models/contracts/contracts';
class StubStatusView implements IStatusView {
installingService(): void {
console.log('...');
}
serviceInstalled(): void {
console.log('Service installed');
}
serviceInstallationFailed(): void {
console.log('Service installation failed');
}
updateServiceDownloadingProgress(downloadPercentage: number): void {
if (downloadPercentage === 100) {
process.stdout.write('100%');
}
}
}
class StubLogger implements ILogger {
logDebug(message: string): void {
console.log(message);
}
increaseIndent(): void {
console.log('increaseIndent');
}
decreaseIndent(): void {
console.log('decreaseIndent');
}
append(message?: string): void {
process.stdout.write(message);
}
appendLine(message?: string): void {
console.log(message);
}
}
export class ServiceInstaller {
private _config = undefined;
private _logger = new StubLogger();
private _statusView = new StubStatusView();
private _httpClient = new HttpClient();
private _decompressProvider = new DecompressProvider();
private _downloadProvider = undefined;
private _serverProvider = undefined;
private _extensionConstants = undefined;
constructor(extensionConstants: IExtensionConstants) {
this._extensionConstants = extensionConstants;
this._config = new Config(extensionConstants.extensionConfigSectionName, true);
this._downloadProvider = new ServiceDownloadProvider(this._config, this._logger, this._statusView, this._httpClient, this._decompressProvider, extensionConstants, true);
this._serverProvider = new ServerProvider(this._downloadProvider, this._config, this._statusView, extensionConstants.extensionConfigSectionName);
}
/*
* Installs the service for the given platform if it's not already installed.
*/
public installService(): Promise<String> {
return PlatformInformation.getCurrent(this._extensionConstants.getRuntimeId, this._extensionConstants.extensionName).then(platformInfo => {
if (platformInfo.isValidRuntime()) {
return this._serverProvider.getOrDownloadServer(platformInfo.runtimeId);
} else {
throw new Error('unsupported runtime');
}
});
}
/*
* Returns the install folder path for given platform.
*/
public getServiceInstallDirectory(runtime: Runtime): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (runtime === undefined) {
PlatformInformation.getCurrent(this._extensionConstants.getRuntimeId, this._extensionConstants.extensionName).then(platformInfo => {
if (platformInfo.isValidRuntime()) {
resolve(this._downloadProvider.getInstallDirectory(platformInfo.runtimeId));
} else {
reject('unsupported runtime');
}
}).catch(error => {
reject(error);
});
} else {
resolve(this._downloadProvider.getInstallDirectory(runtime));
}
});
}
/*
* Returns the path to the root folder of service install location.
*/
public getServiceInstallDirectoryRoot(runtime: Runtime): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (runtime === undefined) {
PlatformInformation.getCurrent(this._extensionConstants.getRuntimeId, this._extensionConstants.extensionName).then(platformInfo => {
if (platformInfo.isValidRuntime()) {
let directoryPath: string = this._downloadProvider.getInstallDirectoryRoot(platformInfo, this._extensionConstants.extensionName);
directoryPath = directoryPath.replace('\\{#version#}', '');
directoryPath = directoryPath.replace('\\{#platform#}', '');
directoryPath = directoryPath.replace('/{#platform#}', '');
directoryPath = directoryPath.replace('/{#version#}', '');
resolve(directoryPath);
} else {
reject('unsupported runtime');
}
}).catch(error => {
reject(error);
});
} else {
resolve(this._downloadProvider.getInstallDirectory(runtime));
}
});
}
}

View File

@@ -0,0 +1,80 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
'use strict';
import vscode = require('vscode');
export default class ServiceStatus implements vscode.Disposable {
private _progressTimerId: NodeJS.Timer;
private _statusBarItem: vscode.StatusBarItem = undefined;
private durationStatusInMs: number = 1500;
// These need localization
private _serviceStartingMessage: string = `Starting ${this._serviceName}`;
private _serviceStartedMessage: string = `${this._serviceName} started`;
constructor(private _serviceName: string) {
this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
}
public showServiceLoading(): Promise<void> {
return this === undefined ?
Promise.resolve() :
Promise.resolve(this.updateStatusView(this._serviceStartingMessage, true));
}
public showServiceLoaded(): Promise<void> {
return this === undefined ?
Promise.resolve() :
Promise.resolve(this.updateStatusView(this._serviceStartedMessage, false, this.durationStatusInMs));
}
//TODO: This can be merged with the serverStatus code
private showProgress(statusText: string): void {
let index: number = 0;
let progressTicks: string[] = ['.', '..', '...', '....'];
this._progressTimerId = setInterval(() => {
index = (index + 1) % progressTicks.length;
let progressTick = progressTicks[index];
if (this._statusBarItem.text !== this._serviceStartedMessage) {
this._statusBarItem.text = statusText + ' ' + progressTick;
this._statusBarItem.show();
}
}, 400);
}
private updateStatusView(message: string, showAsProgress: boolean = false, disposeAfter: number = -1): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (showAsProgress) {
this.showProgress(message);
}
else {
this._statusBarItem.text = message;
this._statusBarItem.show();
if (this._progressTimerId !== undefined) {
clearInterval(this._progressTimerId);
}
}
if (disposeAfter !== -1) {
setInterval(() => {
this._statusBarItem.hide();
}, disposeAfter);
}
resolve();
});
}
dispose(): void {
if (this._progressTimerId !== undefined) {
clearInterval(this._progressTimerId);
}
this._statusBarItem.dispose();
}
}

View File

@@ -0,0 +1,13 @@
import SqlToolsServiceClient from './languageservice/serviceClient';
import ServerProvider from './languageservice/server';
import VscodeWrapper from './controllers/vscodeWrapper';
import * as SharedConstants from './models/constants';
import * as Utils from './models/utils';
export {SqlToolsServiceClient, VscodeWrapper, SharedConstants, Utils};
export {IExtensionConstants} from './models/contracts/contracts';
export {ILanguageClientHelper} from './models/contracts/languageService';
export {Runtime, PlatformInformation} from './models/platform';
export {Telemetry} from './models/telemetry';
export {LinuxDistribution} from './models/platform';
export {ServiceInstaller} from './languageservice/serviceInstallerUtil';

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//constants
export const configLogDebugInfo: string = 'logDebugInfo';
export const serviceNotCompatibleError: string = "Client is not compatible with the service layer";
export const serviceDownloading: string = "Downloading";
export const serviceInstalling: string = "Installing";
export const unsupportedPlatformErrorMessage: string = "This platform is unsupported and application services may not function correctly";
export const extensionActivated: string = 'activated.';
export const extensionDeactivated: string = 'de-activated.';
export const configEnabled: string = 'enabled';
export const configUseDebugSource = 'useDebugSource';
export const serviceConfigKey = 'service';
export const executableFilesConfigKey = 'executableFiles';
export const versionConfigKey = 'version';
export const downloadUrlConfigKey = 'downloadUrl';
export const installDirConfigKey = 'installDir';
export const serviceCrashButton = "View Known Issues";
export const configDebugSourcePath = 'debugSourcePath';
export const neverShowAgain = "Don't show again";
export const ignorePlatformWarning = 'ignorePlatformWarning';
export const usingDefaultPlatformMessage = "Unknown platform detected, defaulting to Linux_x64 platform";
export const serverConnectionMetadata = "serverConnectionMetadata";

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict'
import {RequestType} from 'dataprotocol-client';
import {Runtime, LinuxDistribution} from '../platform';
// --------------------------------- < Version Request > -------------------------------------------------
// Version request message callback declaration
export namespace VersionRequest {
export const type: RequestType<void, VersionResult, void> = { get method(): string { return 'version'; } };
}
// Version response format
export type VersionResult = string;
// ------------------------------- </ Version Request > --------------------------------------------------
// Constants interface for each extension
export interface IExtensionConstants {
// TODO: Fill in interface
// Definitely dependent on the extension
extensionName: string;
invalidServiceFilePath: string;
serviceName: string;
extensionConfigSectionName: string;
serviceCompatibleVersion: string;
outputChannelName: string;
languageId: string;
serviceInstallingTo: string;
serviceInitializing: string;
serviceInstalled: string;
serviceLoadingFailed: string;
serviceInstallationFailed: string;
serviceInitializingOutputChannelName: string;
commandsNotAvailableWhileInstallingTheService : string;
providerId: string;
serviceCrashMessage: string;
serviceCrashLink: string;
installFolderName: string;
telemetryExtensionName: string;
getRuntimeId(platform: string, architecture: string, distribution: LinuxDistribution): Runtime;
}

View File

@@ -0,0 +1,55 @@
import {NotificationType, ServerOptions} from 'dataprotocol-client';
import {ITelemetryEventProperties, ITelemetryEventMeasures} from '../telemetry';
import {Runtime} from '../platform';
// ------------------------------- < Telemetry Sent Event > ------------------------------------
/**
* Event sent when the language service send a telemetry event
*/
export namespace TelemetryNotification {
export const type: NotificationType<TelemetryParams> = { get method(): string { return 'telemetry/sqlevent'; } };
}
/**
* Update event parameters
*/
export class TelemetryParams {
public params: {
eventName: string;
properties: ITelemetryEventProperties;
measures: ITelemetryEventMeasures;
};
}
// ------------------------------- </ Telemetry Sent Event > ----------------------------------
// ------------------------------- < Status Event > ------------------------------------
/**
* Event sent when the language service send a status change event
*/
export namespace StatusChangedNotification {
export const type: NotificationType<StatusChangeParams> = { get method(): string { return 'textDocument/statusChanged'; } };
}
/**
* Update event parameters
*/
export class StatusChangeParams {
/**
* URI identifying the text document
*/
public ownerUri: string;
/**
* The new status of the document
*/
public status: string;
}
// ------------------------------- </ Status Sent Event > ----------------------------------
export interface ILanguageClientHelper {
createServerOptions(servicePath: string, runtimeId?: Runtime): ServerOptions;
}

View File

@@ -0,0 +1,13 @@
'use strict';
export interface ILogger {
logDebug(message: string): void;
increaseIndent(): void;
decreaseIndent(): void;
append(message?: string): void;
appendLine(message?: string): void;
}
export interface IRuntime {
getRuntimeDisplayName();
}

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* 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 {ILogger} from './interfaces';
import * as Utils from './utils';
import {IExtensionConstants} from './contracts/contracts';
/*
* Logger class handles logging messages using the Util functions.
*/
export class Logger implements ILogger {
private _writer: (message: string) => void;
private _prefix: string;
private _extensionConstants: IExtensionConstants;
private _indentLevel: number = 0;
private _indentSize: number = 4;
private _atLineStart: boolean = false;
constructor(writer: (message: string) => void, extensionConstants: IExtensionConstants, prefix?: string) {
this._writer = writer;
this._prefix = prefix;
this._extensionConstants = extensionConstants;
}
public logDebug(message: string): void {
Utils.logDebug(message, this._extensionConstants.extensionConfigSectionName);
}
private _appendCore(message: string): void {
if (this._atLineStart) {
if (this._indentLevel > 0) {
const indent = ' '.repeat(this._indentLevel * this._indentSize);
this._writer(indent);
}
if (this._prefix) {
this._writer(`[${this._prefix}] `);
}
this._atLineStart = false;
}
this._writer(message);
}
public increaseIndent(): void {
this._indentLevel += 1;
}
public decreaseIndent(): void {
if (this._indentLevel > 0) {
this._indentLevel -= 1;
}
}
public append(message?: string): void {
message = message || '';
this._appendCore(message);
}
public appendLine(message?: string): void {
message = message || '';
this._appendCore(message + os.EOL);
this._atLineStart = true;
}
}

View File

@@ -0,0 +1,369 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as child_process from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
const unknown = 'unknown';
export enum Runtime {
UnknownRuntime = <any>'Unknown',
UnknownVersion = <any>'Unknown',
Windows_86 = <any>'Windows_86',
Windows_64 = <any>'Windows_64',
OSX = <any> 'OSX',
CentOS_7 = <any>'CentOS_7',
Debian_8 = <any>'Debian_8',
Fedora_23 = <any>'Fedora_23',
OpenSUSE_13_2 = <any>'OpenSUSE_13_2',
SLES_12_2 = <any>'SLES_12_2',
RHEL_7 = <any>'RHEL_7',
Ubuntu_14 = <any>'Ubuntu_14',
Ubuntu_16 = <any>'Ubuntu_16',
Linux_64 = <any>'Linux_64',
Linux_86 = <any>'Linux-86'
}
export function getRuntimeDisplayName(runtime: Runtime): string {
switch (runtime) {
case Runtime.Windows_64:
return 'Windows';
case Runtime.Windows_86:
return 'Windows';
case Runtime.OSX:
return 'OSX';
case Runtime.CentOS_7:
return 'Linux';
case Runtime.Debian_8:
return 'Linux';
case Runtime.Fedora_23:
return 'Linux';
case Runtime.OpenSUSE_13_2:
return 'Linux';
case Runtime.SLES_12_2:
return 'Linux';
case Runtime.RHEL_7:
return 'Linux';
case Runtime.Ubuntu_14:
return 'Linux';
case Runtime.Ubuntu_16:
return 'Linux';
case Runtime.Linux_64:
return 'Linux';
case Runtime.Linux_86:
return 'Linux';
default:
return 'Unknown';
}
}
export class PlatformInformation {
public runtimeId: Runtime;
public constructor(
public platform: string,
public architecture: string,
public distribution: LinuxDistribution = undefined,
public getRuntimeId: (platform: string, architecture: string, distribution: LinuxDistribution) => Runtime) {
try {
this.runtimeId = this.getRuntimeId(platform, architecture, distribution);
} catch (err) {
this.runtimeId = undefined;
}
}
public isWindows(): boolean {
return this.platform === 'win32';
}
public isMacOS(): boolean {
return this.platform === 'darwin';
}
public isLinux(): boolean {
return this.platform === 'linux';
}
public isValidRuntime(): boolean {
return this.runtimeId !== undefined && this.runtimeId !== Runtime.UnknownRuntime && this.runtimeId !== Runtime.UnknownVersion;
}
public getRuntimeDisplayName(): string {
return getRuntimeDisplayName(this.runtimeId);
}
public toString(): string {
let result = this.platform;
if (this.architecture) {
if (result) {
result += ', ';
}
result += this.architecture;
}
if (this.distribution) {
if (result) {
result += ', ';
}
result += this.distribution.toString();
}
return result;
}
public static getCurrent(getRuntimeId: (platform: string, architecture: string, distribution: LinuxDistribution) => Runtime,
extensionName: string): Promise<any> {
let platform = os.platform();
let architecturePromise: Promise<string>;
let distributionPromise: Promise<LinuxDistribution>;
switch (platform) {
case 'win32':
architecturePromise = PlatformInformation.getWindowsArchitecture();
distributionPromise = Promise.resolve(undefined);
break;
case 'darwin':
let osVersion = os.release();
if (parseFloat(osVersion) < 16.0 && extensionName === 'mssql') {
return Promise.reject('The current version of macOS is not supported. Only macOS Sierra and above (>= 10.12) are supported.')
}
architecturePromise = PlatformInformation.getUnixArchitecture();
distributionPromise = Promise.resolve(undefined);
break;
case 'linux':
architecturePromise = PlatformInformation.getUnixArchitecture();
distributionPromise = LinuxDistribution.getCurrent();
break;
default:
return Promise.reject(`Unsupported platform: ${platform}`);
}
return architecturePromise.then( arch => {
return distributionPromise.then(distro => {
return new PlatformInformation(platform, arch, distro, getRuntimeId);
});
});
}
private static getWindowsArchitecture(): Promise<string> {
return new Promise<string>((resolve, reject) => {
// try to get the architecture from WMIC
PlatformInformation.getWindowsArchitectureWmic().then(architecture => {
if (architecture && architecture !== unknown) {
resolve(architecture);
} else {
// sometimes WMIC isn't available on the path so then try to parse the envvar
PlatformInformation.getWindowsArchitectureEnv().then(architecture => {
resolve(architecture);
});
}
});
});
}
private static getWindowsArchitectureWmic(): Promise<string> {
return this.execChildProcess('wmic os get osarchitecture')
.then(architecture => {
if (architecture) {
let archArray: string[] = architecture.split(os.EOL);
if (archArray.length >= 2) {
let arch = archArray[1].trim();
// Note: This string can be localized. So, we'll just check to see if it contains 32 or 64.
if (arch.indexOf('64') >= 0) {
return 'x86_64';
} else if (arch.indexOf('32') >= 0) {
return 'x86';
}
}
}
return unknown;
}).catch((error) => {
return unknown;
});
}
private static getWindowsArchitectureEnv(): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (process.env.PROCESSOR_ARCHITECTURE === 'x86' && process.env.PROCESSOR_ARCHITEW6432 === undefined) {
resolve('x86');
}
else {
resolve('x86_64');
}
});
}
private static getUnixArchitecture(): Promise<string> {
return this.execChildProcess('uname -m')
.then(architecture => {
if (architecture) {
return architecture.trim();
}
return undefined;
});
}
private static execChildProcess(process: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
child_process.exec(process, { maxBuffer: 500 * 1024 }, (error: Error, stdout: string, stderr: string) => {
if (error) {
reject(error);
return;
}
if (stderr && stderr.length > 0) {
reject(new Error(stderr));
return;
}
resolve(stdout);
});
});
}
private static getRuntimeIdHelper(distributionName: string, distributionVersion: string): Runtime {
switch (distributionName) {
case 'ubuntu':
if (distributionVersion.startsWith('14')) {
// This also works for Linux Mint
return Runtime.Ubuntu_14;
} else if (distributionVersion.startsWith('16')) {
return Runtime.Ubuntu_16;
}
break;
case 'elementary':
case 'elementary OS':
if (distributionVersion.startsWith('0.3')) {
// Elementary OS 0.3 Freya is binary compatible with Ubuntu 14.04
return Runtime.Ubuntu_14;
} else if (distributionVersion.startsWith('0.4')) {
// Elementary OS 0.4 Loki is binary compatible with Ubuntu 16.04
return Runtime.Ubuntu_16;
}
break;
case 'linuxmint':
if (distributionVersion.startsWith('18')) {
// Linux Mint 18 is binary compatible with Ubuntu 16.04
return Runtime.Ubuntu_16;
}
break;
case 'centos':
case 'ol':
// Oracle Linux is binary compatible with CentOS
return Runtime.CentOS_7;
case 'fedora':
return Runtime.Fedora_23;
case 'opensuse':
return Runtime.OpenSUSE_13_2;
case 'sles':
return Runtime.SLES_12_2;
case 'rhel':
return Runtime.RHEL_7;
case 'debian':
return Runtime.Debian_8;
case 'galliumos':
if (distributionVersion.startsWith('2.0')) {
return Runtime.Ubuntu_16;
}
break;
default:
return Runtime.Linux_64;
}
return Runtime.Linux_64;
}
}
/**
* There is no standard way on Linux to find the distribution name and version.
* Recently, systemd has pushed to standardize the os-release file. This has
* seen adoption in "recent" versions of all major distributions.
* https://www.freedesktop.org/software/systemd/man/os-release.html
*/
export class LinuxDistribution {
public constructor(
public name: string,
public version: string,
public idLike?: string[]) { }
public static getCurrent(): Promise<LinuxDistribution> {
// Try /etc/os-release and fallback to /usr/lib/os-release per the synopsis
// at https://www.freedesktop.org/software/systemd/man/os-release.html.
return LinuxDistribution.fromFilePath('/etc/os-release')
.catch(() => LinuxDistribution.fromFilePath('/usr/lib/os-release'))
.catch(() => Promise.resolve(new LinuxDistribution(unknown, unknown)));
}
public toString(): string {
return `name=${this.name}, version=${this.version}`;
}
private static fromFilePath(filePath: string): Promise<LinuxDistribution> {
return new Promise<LinuxDistribution>((resolve, reject) => {
fs.readFile(filePath, 'utf8', (error, data) => {
if (error) {
reject(error);
} else {
resolve(LinuxDistribution.fromReleaseInfo(data));
}
});
});
}
public static fromReleaseInfo(releaseInfo: string, eol: string = os.EOL): LinuxDistribution {
let name = unknown;
let version = unknown;
let idLike: string[] = undefined;
const lines = releaseInfo.split(eol);
for (let line of lines) {
line = line.trim();
let equalsIndex = line.indexOf('=');
if (equalsIndex >= 0) {
let key = line.substring(0, equalsIndex);
let value = line.substring(equalsIndex + 1);
// Strip quotes if necessary
if (value.length > 1 && value.startsWith('"') && value.endsWith('"')) {
value = value.substring(1, value.length - 1);
} else if (value.length > 1 && value.startsWith('\'') && value.endsWith('\'')) {
value = value.substring(1, value.length - 1);
}
if (key === 'ID') {
name = value;
} else if (key === 'VERSION_ID') {
version = value;
} else if (key === 'ID_LIKE') {
idLike = value.split(' ');
}
if (name !== unknown && version !== unknown && idLike !== undefined) {
break;
}
}
}
return new LinuxDistribution(name, version, idLike);
}
}

View File

@@ -0,0 +1,161 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import vscode = require('vscode');
import TelemetryReporter from 'vscode-extension-telemetry';
import Utils = require('./utils');
import { PlatformInformation, Runtime, LinuxDistribution } from './platform';
import { IExtensionConstants } from './contracts/contracts';
export interface ITelemetryEventProperties {
[key: string]: string;
}
export interface ITelemetryEventMeasures {
[key: string]: number;
}
/**
* Filters error paths to only include source files. Exported to support testing
*/
export function FilterErrorPath(line: string): string {
if (line) {
let values: string[] = line.split('/out/');
if (values.length <= 1) {
// Didn't match expected format
return line;
} else {
return values[1];
}
}
}
export class Telemetry {
private static reporter: TelemetryReporter;
private static userId: string;
private static platformInformation: PlatformInformation;
private static disabled: boolean;
private static _getRuntimeId: (platform: string, architecture: string, distribution: LinuxDistribution) => Runtime;
public static get getRuntimeId() {
return this._getRuntimeId;
}
public static set getRuntimeId(runtimeIdGetter: (platform: string, architecture: string, distribution: LinuxDistribution) => Runtime) {
this._getRuntimeId = runtimeIdGetter;
}
// Get the unique ID for the current user of the extension
public static getUserId(): Promise<string> {
return new Promise<string>(resolve => {
// Generate the user id if it has not been created already
if (typeof this.userId === 'undefined') {
let id = Utils.generateUserId();
id.then(newId => {
this.userId = newId;
resolve(this.userId);
});
} else {
resolve(this.userId);
}
});
}
public static getPlatformInformation(): Promise<PlatformInformation> {
if (this.platformInformation) {
return Promise.resolve(this.platformInformation);
} else {
return new Promise<PlatformInformation>(resolve => {
PlatformInformation.getCurrent(this.getRuntimeId, 'telemetry').then(info => {
this.platformInformation = info;
resolve(this.platformInformation);
});
});
}
}
/**
* Disable telemetry reporting
*/
public static disable(): void {
this.disabled = true;
}
/**
* Initialize the telemetry reporter for use.
*/
public static initialize(context: vscode.ExtensionContext, extensionConstants: IExtensionConstants): void {
if (typeof this.reporter === 'undefined') {
// Check if the user has opted out of telemetry
if (!vscode.workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) {
this.disable();
return;
}
let packageInfo = Utils.getPackageInfo(context);
this.reporter = new TelemetryReporter(extensionConstants.telemetryExtensionName, packageInfo.version, packageInfo.aiKey);
}
}
/**
* Send a telemetry event for an exception
*/
public static sendTelemetryEventForException(
err: any, methodName: string, extensionConfigName: string): void {
try {
let stackArray: string[];
let firstLine: string = '';
if (err !== undefined && err.stack !== undefined) {
stackArray = err.stack.split('\n');
if (stackArray !== undefined && stackArray.length >= 2) {
firstLine = stackArray[1]; // The fist line is the error message and we don't want to send that telemetry event
firstLine = FilterErrorPath(firstLine);
}
}
// Only adding the method name and the fist line of the stack trace. We don't add the error message because it might have PII
this.sendTelemetryEvent('Exception', { methodName: methodName, errorLine: firstLine });
Utils.logDebug('Unhandled Exception occurred. error: ' + err + ' method: ' + methodName, extensionConfigName);
} catch (telemetryErr) {
// If sending telemetry event fails ignore it so it won't break the extension
Utils.logDebug('Failed to send telemetry event. error: ' + telemetryErr, extensionConfigName);
}
}
/**
* Send a telemetry event using application insights
*/
public static sendTelemetryEvent(
eventName: string,
properties?: ITelemetryEventProperties,
measures?: ITelemetryEventMeasures): void {
if (typeof this.disabled === 'undefined') {
this.disabled = false;
}
if (this.disabled || typeof (this.reporter) === 'undefined') {
// Don't do anything if telemetry is disabled
return;
}
if (!properties || typeof properties === 'undefined') {
properties = {};
}
// Augment the properties structure with additional common properties before sending
Promise.all([this.getUserId, this.getPlatformInformation]).then(() => {
properties['userId'] = this.userId;
properties['distribution'] = (this.platformInformation && this.platformInformation.distribution) ?
`${this.platformInformation.distribution.name}, ${this.platformInformation.distribution.version}` : '';
this.reporter.sendTelemetryEvent(eventName, properties, measures);
});
}
}
export default Telemetry;

View File

@@ -0,0 +1,270 @@
'use strict';
import * as crypto from 'crypto';
import * as os from 'os';
import vscode = require('vscode');
import Constants = require('./constants');
import {ExtensionContext} from 'vscode';
import fs = require('fs');
var path = require('path');
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
const msInH = 3.6e6;
const msInM = 60000;
const msInS = 1000;
// INTERFACES /////////////////////////////////////////////////////////////////////////////////////
// Interface for package.json information
export interface IPackageInfo {
name: string;
version: string;
aiKey: string;
}
// FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////
// Get information from the extension's package.json file
export function getPackageInfo(context: ExtensionContext): IPackageInfo {
let extensionPackage = require(context.asAbsolutePath('./package.json'));
if (extensionPackage) {
return {
name: extensionPackage.name,
version: extensionPackage.version,
aiKey: extensionPackage.aiKey
};
}
}
// Generate a new GUID
export function generateGuid(): string {
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
let oct: string = '';
let tmp: number;
/* tslint:disable:no-bitwise */
for (let a: number = 0; a < 4; a++) {
tmp = (4294967296 * Math.random()) | 0;
oct += hexValues[tmp & 0xF] +
hexValues[tmp >> 4 & 0xF] +
hexValues[tmp >> 8 & 0xF] +
hexValues[tmp >> 12 & 0xF] +
hexValues[tmp >> 16 & 0xF] +
hexValues[tmp >> 20 & 0xF] +
hexValues[tmp >> 24 & 0xF] +
hexValues[tmp >> 28 & 0xF];
}
// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
/* tslint:enable:no-bitwise */
}
// Generate a unique, deterministic ID for the current user of the extension
export function generateUserId(): Promise<string> {
return new Promise<string>(resolve => {
try {
let interfaces = os.networkInterfaces();
let mac;
for(let key of Object.keys(interfaces)) {
let item = interfaces[key][0];
if (!item.internal) {
mac = item.mac;
break;
}
}
if (mac) {
resolve(crypto.createHash('sha256').update(mac + os.homedir(), 'utf8').digest('hex'));
} else {
resolve(generateGuid());
}
} catch (err) {
resolve(generateGuid()); // fallback
}
});
}
// Return 'true' if the active editor window has a .sql file, false otherwise
export function isEditingSqlFile(languageId: string): boolean {
let sqlFile = false;
let editor = getActiveTextEditor();
if (editor) {
if (editor.document.languageId === languageId) {
sqlFile = true;
}
}
return sqlFile;
}
// Return the active text editor if there's one
export function getActiveTextEditor(): vscode.TextEditor {
let editor = undefined;
if (vscode.window && vscode.window.activeTextEditor) {
editor = vscode.window.activeTextEditor;
}
return editor;
}
// Retrieve the URI for the currently open file if there is one; otherwise return the empty string
export function getActiveTextEditorUri(): string {
if (typeof vscode.window.activeTextEditor !== 'undefined' &&
typeof vscode.window.activeTextEditor.document !== 'undefined') {
return vscode.window.activeTextEditor.document.uri.toString();
}
return '';
}
// Helper to log messages to output channel
export function logToOutputChannel(msg: any, outputChannelName: string): void {
let outputChannel = vscode.window.createOutputChannel(outputChannelName);
outputChannel.show();
if (msg instanceof Array) {
msg.forEach(element => {
outputChannel.appendLine(element.toString());
});
} else {
outputChannel.appendLine(msg.toString());
}
}
// Helper to log debug messages
export function logDebug(msg: any, extensionConfigSectionName: string): void {
let config = vscode.workspace.getConfiguration(extensionConfigSectionName);
let logDebugInfo = config[Constants.configLogDebugInfo];
if (logDebugInfo === true) {
let currentTime = new Date().toLocaleTimeString();
let outputMsg = '[' + currentTime + ']: ' + msg ? msg.toString() : '';
console.log(outputMsg);
}
}
// Helper to show an info message
export function showInfoMsg(msg: string, extensionName: string): void {
vscode.window.showInformationMessage(extensionName + ': ' + msg );
}
// Helper to show an warn message
export function showWarnMsg(msg: string, extensionName: string): void {
vscode.window.showWarningMessage(extensionName + ': ' + msg );
}
// Helper to show an error message
export function showErrorMsg(msg: string, extensionName: string): void {
vscode.window.showErrorMessage(extensionName + ': ' + msg );
}
export function isEmpty(str: any): boolean {
return (!str || '' === str);
}
export function isNotEmpty(str: any): boolean {
return <boolean>(str && '' !== str);
}
/**
* Format a string. Behaves like C#'s string.Format() function.
*/
export function formatString(str: string, ...args: any[]): string {
// This is based on code originally from https://github.com/Microsoft/vscode/blob/master/src/vs/nls.js
// License: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
let result: string;
if (args.length === 0) {
result = str;
} else {
result = str.replace(/\{(\d+)\}/g, (match, rest) => {
let index = rest[0];
return typeof args[index] !== 'undefined' ? args[index] : match;
});
}
return result;
}
/**
* Check if a file exists on disk
*/
export function isFileExisting(filePath: string): boolean {
try {
fs.statSync(filePath);
return true;
} catch (err) {
return false;
}
}
/**
* Takes a string in the format of HH:MM:SS.MS and returns a number representing the time in
* miliseconds
* @param value The string to convert to milliseconds
* @return False is returned if the string is an invalid format,
* the number of milliseconds in the time string is returned otherwise.
*/
export function parseTimeString(value: string): number | boolean {
if (!value) {
return false;
}
let tempVal = value.split('.');
if (tempVal.length !== 2) {
return false;
}
let ms = parseInt(tempVal[1].substring(0, 3), 10);
tempVal = tempVal[0].split(':');
if (tempVal.length !== 3) {
return false;
}
let h = parseInt(tempVal[0], 10);
let m = parseInt(tempVal[1], 10);
let s = parseInt(tempVal[2], 10);
return ms + (h * msInH) + (m * msInM) + (s * msInS);
}
/**
* Takes a number of milliseconds and converts it to a string like HH:MM:SS.fff
* @param value The number of milliseconds to convert to a timespan string
* @returns A properly formatted timespan string.
*/
export function parseNumAsTimeString(value: number): string {
let tempVal = value;
let h = Math.floor(tempVal / msInH);
tempVal %= msInH;
let m = Math.floor(tempVal / msInM);
tempVal %= msInM;
let s = Math.floor(tempVal / msInS);
tempVal %= msInS;
let hs = h < 10 ? '0' + h : '' + h;
let ms = m < 10 ? '0' + m : '' + m;
let ss = s < 10 ? '0' + s : '' + s;
let mss = tempVal < 10 ? '00' + tempVal : tempVal < 100 ? '0' + tempVal : '' + tempVal;
let rs = hs + ':' + ms + ':' + ss;
return tempVal > 0 ? rs + '.' + mss : rs;
}
// The function is a duplicate of carbon\src\paths.js. IT would be better to import path.js but it doesn't
// work for now because the extension is running in different process.
export function getAppDataPath() {
var platform = process.platform;
switch (platform) {
case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming');
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support');
case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
default: throw new Error('Platform not supported');
}
}
export function getDefaultLogLocation() {
var platform = process.platform;
let rootFolderName: string = '.sqlops';
if (platform === 'win32') {
rootFolderName = 'sqlops';
}
return path.join(getAppDataPath(), rootFolderName);
}

View File

@@ -0,0 +1,19 @@
{
"compileOnSave": true,
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "../lib",
"lib": [
"es6", "es2015.promise"
],
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"declaration": true
},
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../src/sql/data.d.ts'/>

47
extensions-modules/src/typings/tmp.d.ts vendored Normal file
View File

@@ -0,0 +1,47 @@
// Type definitions for tmp v0.0.28
// Project: https://www.npmjs.com/package/tmp
// Definitions by: Jared Klopper <https://github.com/optical>
declare module "tmp" {
module tmp {
interface Options extends SimpleOptions {
mode?: number;
}
interface SimpleOptions {
prefix?: string;
postfix?: string;
template?: string;
dir?: string;
tries?: number;
keep?: boolean;
unsafeCleanup?: boolean;
}
interface SynchronousResult {
name: string;
fd: number;
removeCallback: () => void;
}
function file(callback: (err: any, path: string, fd: number, cleanupCallback: () => void) => void): void;
function file(config: Options, callback?: (err: any, path: string, fd: number, cleanupCallback: () => void) => void): void;
function fileSync(config?: Options): SynchronousResult;
function dir(callback: (err: any, path: string, cleanupCallback: () => void) => void): void;
function dir(config: Options, callback?: (err: any, path: string, cleanupCallback: () => void) => void): void;
function dirSync(config?: Options): SynchronousResult;
function tmpName(callback: (err: any, path: string) => void): void;
function tmpName(config: SimpleOptions, callback?: (err: any, path: string) => void): void;
function tmpNameSync(config?: SimpleOptions): string;
function setGracefulCleanup(): void;
}
export = tmp;
}

View File

@@ -0,0 +1,6 @@
declare module 'vscode-extension-telemetry' {
export default class TelemetryReporter {
constructor(extensionId: string, extensionVersion: string, key: string);
sendTelemetryEvent(eventName: string, properties?: { [key: string]: string }, measures?: { [key: string]: number }): void;
}
}

View File

@@ -0,0 +1,3 @@
'use strict';
export default require('error-ex')('EscapeException');

View File

@@ -0,0 +1,3 @@
'use strict';
export default require('error-ex')('ValidationException');

View File

@@ -0,0 +1,142 @@
import vscode = require('vscode');
import * as Utils from '../models/utils';
// Status bar element for each file in the editor
class FileStatusBar {
// Item for the connection status
public statusConnection: vscode.StatusBarItem;
// Item for the query status
public statusQuery: vscode.StatusBarItem;
// Item for language service status
public statusLanguageService: vscode.StatusBarItem;
// Timer used for displaying a progress indicator on queries
public progressTimerId: NodeJS.Timer;
public currentLanguageServiceStatus: string;
}
export default class StatusView implements vscode.Disposable {
private _statusBars: { [fileUri: string]: FileStatusBar };
private _lastShownStatusBar: FileStatusBar;
constructor() {
this._statusBars = {};
vscode.window.onDidChangeActiveTextEditor((params) => this.onDidChangeActiveTextEditor(params));
vscode.workspace.onDidCloseTextDocument((params) => this.onDidCloseTextDocument(params));
}
dispose(): void {
for (let bar in this._statusBars) {
if (this._statusBars.hasOwnProperty(bar)) {
this._statusBars[bar].statusConnection.dispose();
this._statusBars[bar].statusQuery.dispose();
this._statusBars[bar].statusLanguageService.dispose();
clearInterval(this._statusBars[bar].progressTimerId);
delete this._statusBars[bar];
}
}
}
// Create status bar item if needed
private createStatusBar(fileUri: string): void {
let bar = new FileStatusBar();
bar.statusConnection = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
bar.statusQuery = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
bar.statusLanguageService = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
this._statusBars[fileUri] = bar;
}
private destroyStatusBar(fileUri: string): void {
let bar = this._statusBars[fileUri];
if (bar) {
if (bar.statusConnection) {
bar.statusConnection.dispose();
}
if (bar.statusQuery) {
bar.statusQuery.dispose();
}
if (bar.statusLanguageService) {
bar.statusLanguageService.dispose();
}
if (bar.progressTimerId) {
clearInterval(bar.progressTimerId);
}
delete this._statusBars[fileUri];
}
}
private getStatusBar(fileUri: string): FileStatusBar {
if (!(fileUri in this._statusBars)) {
// Create it if it does not exist
this.createStatusBar(fileUri);
}
let bar = this._statusBars[fileUri];
if (bar.progressTimerId) {
clearInterval(bar.progressTimerId);
}
return bar;
}
public languageServiceStatusChanged(fileUri: string, status: string): void {
let bar = this.getStatusBar(fileUri);
bar.currentLanguageServiceStatus = status;
this.updateStatusMessage(status,
() => { return bar.currentLanguageServiceStatus; }, (message) => {
bar.statusLanguageService.text = message;
this.showStatusBarItem(fileUri, bar.statusLanguageService);
});
}
public updateStatusMessage(
newStatus: string,
getCurrentStatus: () => string,
updateMessage: (message: string) => void): void {
}
private hideLastShownStatusBar(): void {
if (typeof this._lastShownStatusBar !== 'undefined') {
this._lastShownStatusBar.statusConnection.hide();
this._lastShownStatusBar.statusQuery.hide();
this._lastShownStatusBar.statusLanguageService.hide();
}
}
private onDidChangeActiveTextEditor(editor: vscode.TextEditor): void {
// Hide the most recently shown status bar
this.hideLastShownStatusBar();
// Change the status bar to match the open file
if (typeof editor !== 'undefined') {
const fileUri = editor.document.uri.toString();
const bar = this._statusBars[fileUri];
if (bar) {
this.showStatusBarItem(fileUri, bar.statusConnection);
this.showStatusBarItem(fileUri, bar.statusLanguageService);
}
}
}
private onDidCloseTextDocument(doc: vscode.TextDocument): void {
// Remove the status bar associated with the document
this.destroyStatusBar(doc.uri.toString());
}
private showStatusBarItem(fileUri: string, statusBarItem: vscode.StatusBarItem): void {
let currentOpenFile = Utils.getActiveTextEditorUri();
// Only show the status bar if it matches the currently open file and is not empty
if (fileUri === currentOpenFile && !Utils.isEmpty(statusBarItem.text) ) {
statusBarItem.show();
if (fileUri in this._statusBars) {
this._lastShownStatusBar = this._statusBars[fileUri];
}
} else {
statusBarItem.hide();
}
}
}