mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-08 17:24:01 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
24
extensions-modules/src/languageservice/decompressProvider.ts
Normal file
24
extensions-modules/src/languageservice/decompressProvider.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
146
extensions-modules/src/languageservice/httpClient.ts
Normal file
146
extensions-modules/src/languageservice/httpClient.ts
Normal 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;
|
||||
}
|
||||
46
extensions-modules/src/languageservice/interfaces.ts
Normal file
46
extensions-modules/src/languageservice/interfaces.ts
Normal 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>;
|
||||
}
|
||||
48
extensions-modules/src/languageservice/proxy.ts
Normal file
48
extensions-modules/src/languageservice/proxy.ts
Normal 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);
|
||||
}
|
||||
112
extensions-modules/src/languageservice/server.ts
Normal file
112
extensions-modules/src/languageservice/server.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
125
extensions-modules/src/languageservice/serverStatus.ts
Normal file
125
extensions-modules/src/languageservice/serverStatus.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
492
extensions-modules/src/languageservice/serviceClient.ts
Normal file
492
extensions-modules/src/languageservice/serviceClient.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
130
extensions-modules/src/languageservice/serviceInstallerUtil.ts
Normal file
130
extensions-modules/src/languageservice/serviceInstallerUtil.ts
Normal 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
80
extensions-modules/src/languageservice/serviceStatus.ts
Normal file
80
extensions-modules/src/languageservice/serviceStatus.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user