From 3ac8451a03388f80f9a55d5387ca42ed963e4726 Mon Sep 17 00:00:00 2001 From: Leila Lali Date: Tue, 22 Nov 2016 17:04:18 -0800 Subject: [PATCH] Added support for more platforms (#400) * Added support for more platforms --- src/configurations/dev.config.json | 18 +- src/configurations/production.config.json | 18 +- src/controllers/connectionManager.ts | 27 +- src/languageservice/download.ts | 17 +- src/languageservice/server.ts | 26 +- src/languageservice/serviceInstallerUtil.ts | 6 +- src/languageservice/serviceclient.ts | 16 +- src/models/platform.ts | 399 ++++++++++++++++---- tasks/packagetasks.js | 24 +- test/download.test.ts | 10 +- test/platform.test.ts | 11 +- test/server.test.ts | 18 +- 12 files changed, 439 insertions(+), 151 deletions(-) diff --git a/src/configurations/dev.config.json b/src/configurations/dev.config.json index 964d9e0639..355f896548 100644 --- a/src/configurations/dev.config.json +++ b/src/configurations/dev.config.json @@ -3,15 +3,15 @@ "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", "version": "0.1.4", "downloadFileNames": { - "Windows": "win-x64-netcoreapp1.0.zip", - "OSX": "osx-x64-netcoreapp1.0.tar.gz", - "CentOS": "centos-x64-netcoreapp1.0.tar.gz", - "Debian": "debian-x64-netcoreapp1.0.tar.gz", - "Fedora": "fedora-x64-netcoreapp1.0.tar.gz", - "OpenSUSE": "opensuse-x64-netcoreapp1.0.tar.gz", - "RHEL": "rhel-x64-netcoreapp1.0.tar.gz", - "Ubuntu14": "ubuntu14-x64-netcoreapp1.0.tar.gz", - "Ubuntu16": "ubuntu16-x64-netcoreapp1.0.tar.gz" + "Windows_7_64": "win-x64-netcoreapp1.0.zip", + "OSX_10_11_64": "osx-x64-netcoreapp1.0.tar.gz", + "CentOS_7": "centos-x64-netcoreapp1.0.tar.gz", + "Debian_8": "debian-x64-netcoreapp1.0.tar.gz", + "Fedora_23": "fedora-x64-netcoreapp1.0.tar.gz", + "OpenSUSE_13_2": "opensuse-x64-netcoreapp1.0.tar.gz", + "RHEL_7": "rhel-x64-netcoreapp1.0.tar.gz", + "Ubuntu_14": "ubuntu14-x64-netcoreapp1.0.tar.gz", + "Ubuntu_16": "ubuntu16-x64-netcoreapp1.0.tar.gz" }, "installDir": "../sqltoolsservice/{#version#}/{#platform#}", "executableFiles": ["Microsoft.SqlTools.ServiceLayer.exe", "Microsoft.SqlTools.ServiceLayer", "Microsoft.SqlTools.ServiceLayer.dll"] diff --git a/src/configurations/production.config.json b/src/configurations/production.config.json index 964d9e0639..355f896548 100644 --- a/src/configurations/production.config.json +++ b/src/configurations/production.config.json @@ -3,15 +3,15 @@ "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", "version": "0.1.4", "downloadFileNames": { - "Windows": "win-x64-netcoreapp1.0.zip", - "OSX": "osx-x64-netcoreapp1.0.tar.gz", - "CentOS": "centos-x64-netcoreapp1.0.tar.gz", - "Debian": "debian-x64-netcoreapp1.0.tar.gz", - "Fedora": "fedora-x64-netcoreapp1.0.tar.gz", - "OpenSUSE": "opensuse-x64-netcoreapp1.0.tar.gz", - "RHEL": "rhel-x64-netcoreapp1.0.tar.gz", - "Ubuntu14": "ubuntu14-x64-netcoreapp1.0.tar.gz", - "Ubuntu16": "ubuntu16-x64-netcoreapp1.0.tar.gz" + "Windows_7_64": "win-x64-netcoreapp1.0.zip", + "OSX_10_11_64": "osx-x64-netcoreapp1.0.tar.gz", + "CentOS_7": "centos-x64-netcoreapp1.0.tar.gz", + "Debian_8": "debian-x64-netcoreapp1.0.tar.gz", + "Fedora_23": "fedora-x64-netcoreapp1.0.tar.gz", + "OpenSUSE_13_2": "opensuse-x64-netcoreapp1.0.tar.gz", + "RHEL_7": "rhel-x64-netcoreapp1.0.tar.gz", + "Ubuntu_14": "ubuntu14-x64-netcoreapp1.0.tar.gz", + "Ubuntu_16": "ubuntu16-x64-netcoreapp1.0.tar.gz" }, "installDir": "../sqltoolsservice/{#version#}/{#platform#}", "executableFiles": ["Microsoft.SqlTools.ServiceLayer.exe", "Microsoft.SqlTools.ServiceLayer", "Microsoft.SqlTools.ServiceLayer.dll"] diff --git a/src/controllers/connectionManager.ts b/src/controllers/connectionManager.ts index 7d30886007..ecd0e921ff 100644 --- a/src/controllers/connectionManager.ts +++ b/src/controllers/connectionManager.ts @@ -14,7 +14,7 @@ import { IPrompter } from '../prompts/question'; import Telemetry from '../models/telemetry'; import VscodeWrapper from './vscodeWrapper'; import {NotificationHandler} from 'vscode-languageclient'; -import {Platform, getCurrentPlatform} from '../models/platform'; +import {Runtime, PlatformInformation} from '../models/platform'; let opener = require('opener'); @@ -289,17 +289,20 @@ export default class ConnectionManager { Utils.showErrorMsg(Utils.formatString(Constants.msgConnectionError, result.errorNumber, result.errorMessage)); } } else { - let platform: Platform = getCurrentPlatform(); - if (platform === Platform.OSX && result.messages.indexOf('Unable to load DLL \'System.Security.Cryptography.Native\'') !== -1) { - this.vscodeWrapper.showErrorMessage(Utils.formatString(Constants.msgConnectionError2, - Constants.macOpenSslErrorMessage), Constants.macOpenSslHelpButton).then(action => { - if (action && action === Constants.macOpenSslHelpButton) { - opener(Constants.macOpenSslHelpLink); - } - }); - } else { - Utils.showErrorMsg(Utils.formatString(Constants.msgConnectionError2, result.messages)); - } + PlatformInformation.GetCurrent().then( platformInfo => { + if (platformInfo.runtimeId === Runtime.OSX_10_11_64 && + result.messages.indexOf('Unable to load DLL \'System.Security.Cryptography.Native\'') !== -1) { + this.vscodeWrapper.showErrorMessage(Utils.formatString(Constants.msgConnectionError2, + Constants.macOpenSslErrorMessage), Constants.macOpenSslHelpButton).then(action => { + if (action && action === Constants.macOpenSslHelpButton) { + opener(Constants.macOpenSslHelpLink); + } + }); + } else { + Utils.showErrorMsg(Utils.formatString(Constants.msgConnectionError2, result.messages)); + } + }); + } this.statusView.connectError(fileUri, connection.credentials, result); this.vscodeWrapper.logToOutputChannel( diff --git a/src/languageservice/download.ts b/src/languageservice/download.ts index ff5dd83b66..ee71e1730a 100644 --- a/src/languageservice/download.ts +++ b/src/languageservice/download.ts @@ -9,7 +9,7 @@ import * as https from 'https'; import * as http from 'http'; import * as stream from 'stream'; import {parse} from 'url'; -import {Platform, getCurrentPlatform} from '../models/platform'; +import {Runtime, getRuntimeDisplayName} from '../models/platform'; import {getProxyAgent} from './proxy'; import * as path from 'path'; import {IConfig, ILogger} from './interfaces'; @@ -33,7 +33,7 @@ export default class ServiceDownloadProvider { /** * Returns the download url for given platfotm */ - public getDownloadFileName(platform: Platform): string { + public getDownloadFileName(platform: Runtime): string { let fileNamesJson = this._config.getSqlToolsConfigValue('downloadFileNames'); let fileName = fileNamesJson[platform.toString()]; @@ -98,14 +98,12 @@ export default class ServiceDownloadProvider { /** * Returns SQL tools service installed folder. */ - public getInstallDirectory(platform?: Platform): string { - if (platform === undefined) { - platform = getCurrentPlatform(); - } + public getInstallDirectory(platform: Runtime): string { + let basePath = this.getInstallDirectoryRoot(); let versionFromConfig = this._config.getSqlToolsPackageVersion(); basePath = basePath.replace('{#version#}', versionFromConfig); - basePath = basePath.replace('{#platform#}', platform.toString()); + basePath = basePath.replace('{#platform#}', getRuntimeDisplayName(platform)); fse.mkdirsSync(basePath); return basePath; } @@ -136,12 +134,9 @@ export default class ServiceDownloadProvider { /** * Downloads the SQL tools service and decompress it in the install folder. */ - public go(platform?: Platform): Promise { + public go(platform: Runtime): Promise { const proxy = this._config.getWorkspaceConfig('http.proxy'); const strictSSL = this._config.getWorkspaceConfig('http.proxyStrictSSL', true); - if (platform === undefined) { - platform = getCurrentPlatform(); - } return new Promise((resolve, reject) => { const fileName = this.getDownloadFileName( platform); diff --git a/src/languageservice/server.ts b/src/languageservice/server.ts index b60aad2cc0..0c971ffd95 100644 --- a/src/languageservice/server.ts +++ b/src/languageservice/server.ts @@ -6,7 +6,7 @@ 'use strict'; import * as path from 'path'; -import {Platform, getCurrentPlatform} from '../models/platform'; +import {Runtime, PlatformInformation} from '../models/platform'; import ServiceDownloadProvider from './download'; import {IConfig, IStatusView, IExtensionWrapper} from './interfaces'; let fs = require('fs-extra-promise'); @@ -54,20 +54,28 @@ export default class ServerProvider { /** * Download the SQL tools service if doesn't exist and returns the file path. */ - public getServerPath(platform: Platform): Promise { + public getServerPath(runtime?: Runtime): Promise { - if (platform === undefined) { - platform = getCurrentPlatform(); + if (runtime === undefined) { + return PlatformInformation.GetCurrent().then( currentPlatform => { + return this.getServerPathForPlatform(currentPlatform.runtimeId); + }); + } else { + return this.getServerPathForPlatform(runtime); } + + } + + private getServerPathForPlatform(runtime: Runtime): Promise { // 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. - const installDirectory = this._downloadProvider.getInstallDirectory(platform); + const installDirectory = this._downloadProvider.getInstallDirectory(runtime); return new Promise((resolve, reject) => { return this.findServerPath(installDirectory).then(result => { if (result === undefined) { - return this.downloadServerFiles(platform).then ( downloadResult => { + return this.downloadServerFiles(runtime).then ( downloadResult => { resolve(downloadResult); }); } else { @@ -81,11 +89,11 @@ export default class ServerProvider { }); } - private downloadServerFiles(platform: Platform): Promise { - const installDirectory = this._downloadProvider.getInstallDirectory(platform); + private downloadServerFiles(runtime: Runtime): Promise { + const installDirectory = this._downloadProvider.getInstallDirectory(runtime); let currentFileUrl = this._vsCodeExtention.getActiveTextEditorUri(); this._statusView.installingService(currentFileUrl); - return this._downloadProvider.go(platform).then( _ => { + return this._downloadProvider.go(runtime).then( _ => { return this.findServerPath(installDirectory).then ( result => { this._statusView.serviceInstalled(currentFileUrl); return result; diff --git a/src/languageservice/serviceInstallerUtil.ts b/src/languageservice/serviceInstallerUtil.ts index a0a99a8a7a..ad3ef10536 100644 --- a/src/languageservice/serviceInstallerUtil.ts +++ b/src/languageservice/serviceInstallerUtil.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import {Platform} from '../models/platform'; +import * as Platform from '../models/platform'; import Config from '../configurations/config'; import ServiceDownloadProvider from './download'; import ServerProvider from './server'; @@ -41,7 +41,7 @@ const stubVsCode = new StubVsCode(); /* * Installs the service for the given platform if it's not already installed. */ -export function installService(platform: Platform): Promise { +export function installService(platform: Platform.Runtime): Promise { let downloadProvider = new ServiceDownloadProvider(config, logger); let serverProvider = new ServerProvider(downloadProvider, config, statusView, stubVsCode); return serverProvider.getServerPath(platform); @@ -50,7 +50,7 @@ export function installService(platform: Platform): Promise { /* * Returns the install folder path for given platform. */ -export function getServiceInstallDirectory(platform: Platform): string { +export function getServiceInstallDirectory(platform: Platform.Runtime): string { let downloadProvider = new ServiceDownloadProvider(config, logger); return downloadProvider.getInstallDirectory(platform); } diff --git a/src/languageservice/serviceclient.ts b/src/languageservice/serviceclient.ts index 33f22acdd8..80132c4db2 100644 --- a/src/languageservice/serviceclient.ts +++ b/src/languageservice/serviceclient.ts @@ -15,7 +15,7 @@ import ServiceDownloadProvider from './download'; import {ExtensionWrapper, Logger} from './extUtil'; import ExtConfig from '../configurations/extConfig'; import StatusView from '../views/statusView'; -import {Platform, getCurrentPlatform} from '../models/platform'; +import {Runtime, PlatformInformation} from '../models/platform'; // The Service Client class handles communication with the VS Code LanguageClient export default class SqlToolsServiceClient { @@ -55,12 +55,18 @@ export default class SqlToolsServiceClient { // initialize the SQL Tools Service Client instance by launching // out-of-proc server through the LanguageClient public initialize(context: ExtensionContext): Promise { - return new Promise( (resolve, reject) => { - const platform = getCurrentPlatform(); - if (platform === Platform.Unknown) { + return PlatformInformation.GetCurrent().then( platformInfo => { + if (platformInfo.runtimeId === Runtime.UnknownRuntime) { throw new Error('Invalid Platform'); + } else { + return this.initializeService(platformInfo.runtimeId, context); } - this._server.getServerPath(platform).then(serverPath => { + }); + } + + private initializeService(runtime: Runtime, context: ExtensionContext): Promise { + return new Promise( (resolve, reject) => { + this._server.getServerPath(runtime).then(serverPath => { let serverArgs = []; let serverCommand = serverPath; if (serverPath === undefined) { diff --git a/src/models/platform.ts b/src/models/platform.ts index 7e632f87b4..9e5ba79dcf 100644 --- a/src/models/platform.ts +++ b/src/models/platform.ts @@ -6,92 +6,365 @@ 'use strict'; import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; -export enum Platform { - Unknown = 'Unknown', - Windows = 'Windows', - OSX = 'OSX', - CentOS = 'CentOS', - Debian = 'Debian', - Fedora = 'Fedora', - OpenSUSE = 'OpenSUSE', - RHEL = 'RHEL', - Ubuntu14 = 'Ubuntu14', - Ubuntu16 = 'Ubuntu16' +const unknown = 'unknown'; + +export enum Runtime { + UnknownRuntime = 'Unknown', + UnknownVersion = 'Unknown', + Windows_7_64 = 'Windows_7_64', + OSX_10_11_64 = 'OSX_10_11_64', + CentOS_7 = 'CentOS_7', + Debian_8 = 'Debian_8', + Fedora_23 = 'Fedora_23', + OpenSUSE_13_2 = 'OpenSUSE_13_2', + RHEL_7 = 'RHEL_7', + Ubuntu_14 = 'Ubuntu_14', + Ubuntu_16 = 'Ubuntu_16' +} + +export function getRuntimeDisplayName(runtime: Runtime): string { + switch (runtime) { + case Runtime.Windows_7_64: + return 'Windows'; + case Runtime.OSX_10_11_64: + return 'OSX'; + case Runtime.CentOS_7: + return 'CentOS'; + case Runtime.Debian_8: + return 'Debian'; + case Runtime.Fedora_23: + return 'Fedora'; + case Runtime.OpenSUSE_13_2: + return 'OpenSUSE'; + case Runtime.RHEL_7: + return 'RHEL'; + case Runtime.Ubuntu_14: + return 'Ubuntu14'; + case Runtime.Ubuntu_16: + return 'Ubuntu16'; + default: + return 'Unknown'; + } } -export function getCurrentPlatform(): Platform { - if (process.platform === 'win32') { - return Platform.Windows; - } else if (process.platform === 'darwin') { - return Platform.OSX; - } else if (process.platform === 'linux') { - // Get the text of /etc/os-release to discover which Linux distribution we're running on. - // For details: https://www.freedesktop.org/software/systemd/man/os-release.html - const text = child_process.execSync('cat /etc/os-release').toString(); - const lines = text.split('\n'); - - function getValue(name: string): String { - for (let line of lines) { - line = line.trim(); - if (line.startsWith(name)) { - const equalsIndex = line.indexOf('='); - if (equalsIndex >= 0) { - let value = line.substring(equalsIndex + 1); - - // Strip double quotes if necessary - if (value.length > 1 && value.startsWith('"') && value.endsWith('"')) { - value = value.substring(1, value.length - 1); +export class PlatformInformation { + public runtimeId: Runtime; + + public constructor( + public platform: string, + public architecture: string, + public distribution: LinuxDistribution = undefined) { + try { + this.runtimeId = PlatformInformation.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 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(): Promise { + let platform = os.platform(); + let architecturePromise: Promise; + let distributionPromise: Promise; + + switch (platform) { + case 'win32': + architecturePromise = PlatformInformation.GetWindowsArchitecture(); + distributionPromise = Promise.resolve(undefined); + break; + + case 'darwin': + architecturePromise = PlatformInformation.GetUnixArchitecture(); + distributionPromise = Promise.resolve(undefined); + break; + + case 'linux': + architecturePromise = PlatformInformation.GetUnixArchitecture(); + distributionPromise = LinuxDistribution.GetCurrent(); + break; + + default: + throw new Error(`Unsupported platform: ${platform}`); + } + + return architecturePromise.then( arch => { + return distributionPromise.then(distro => { + return new PlatformInformation(platform, arch, distro); + }); + }); + } + + private static GetWindowsArchitecture(): Promise { + 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 GetUnixArchitecture(): Promise { + return this.execChildProcess('uname -m') + .then(architecture => { + if (architecture) { + return architecture.trim(); + } + + return undefined; + }); + } + + private static execChildProcess(process: string): Promise { + return new Promise((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); + }); + }); + } + + /** + * Returns a supported .NET Core Runtime ID (RID) for the current platform. The list of Runtime IDs + * is available at https://github.com/dotnet/corefx/tree/master/pkg/Microsoft.NETCore.Platforms. + */ + private static getRuntimeId(platform: string, architecture: string, distribution: LinuxDistribution): Runtime { + // Note: We could do much better here. Currently, we only return a limited number of RIDs that + // are officially supported. + + switch (platform) { + case 'win32': + switch (architecture) { + case 'x86': return Runtime.UnknownRuntime; + case 'x86_64': return Runtime.Windows_7_64; + default: + } - return value; + throw new Error(`Unsupported Windows architecture: ${architecture}`); + + case 'darwin': + if (architecture === 'x86_64') { + // Note: We return the El Capitan RID for Sierra + return Runtime.OSX_10_11_64; + } + + throw new Error(`Unsupported macOS architecture: ${architecture}`); + + case 'linux': + if (architecture === 'x86_64') { + + // First try the distribution name + let runtimeId = PlatformInformation.getRuntimeIdHelper(distribution.name, distribution.version); + + // If the distribution isn't one that we understand, but the 'ID_LIKE' field has something that we understand, use that + // + // NOTE: 'ID_LIKE' doesn't specify the version of the 'like' OS. So we will use the 'VERSION_ID' value. This will restrict + // how useful ID_LIKE will be since it requires the version numbers to match up, but it is the best we can do. + if (runtimeId === Runtime.UnknownRuntime && distribution.idLike && distribution.idLike.length > 0) { + for (let id of distribution.idLike) { + runtimeId = PlatformInformation.getRuntimeIdHelper(id, distribution.version); + if (runtimeId !== Runtime.UnknownRuntime) { + break; + } + } + } + + if (runtimeId !== Runtime.UnknownRuntime && runtimeId !== Runtime.UnknownVersion) { + return runtimeId; } } - } - return undefined; + // If we got here, this is not a Linux distro or architecture that we currently support. + throw new Error(`Unsupported Linux distro: ${distribution.name}, ${distribution.version}, ${architecture}`); + default : + // If we got here, we've ended up with a platform we don't support like 'freebsd' or 'sunos'. + // Chances are, VS Code doesn't support these platforms either. + throw Error('Unsupported platform ' + platform); } + } - const id = getValue('ID'); - - switch (id) { + private static getRuntimeIdHelper(distributionName: string, distributionVersion: string): Runtime { + switch (distributionName) { case 'ubuntu': - const versionId = getValue('VERSION_ID'); - if (versionId.startsWith('14')) { + if (distributionVersion.startsWith('14')) { // This also works for Linux Mint - return Platform.Ubuntu14; - } else if (versionId.startsWith('16')) { - return Platform.Ubuntu16; + 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': - return Platform.CentOS; + case 'ol': + // Oracle Linux is binary compatible with CentOS + return Runtime.CentOS_7; case 'fedora': - return Platform.Fedora; + return Runtime.Fedora_23; case 'opensuse': - return Platform.OpenSUSE; + return Runtime.OpenSUSE_13_2; case 'rhel': - return Platform.RHEL; + return Runtime.RHEL_7; case 'debian': - return Platform.Debian; - case 'ol': - // Oracle Linux is binary compatible with CentOS - return Platform.CentOS; - case 'elementary OS': - case 'elementary': - const eOSVersionId = getValue('VERSION_ID'); - if (eOSVersionId.startsWith('0.3')) { - // Elementary OS 0.3 Freya is binary compatible with Ubuntu 14.04 - return Platform.Ubuntu14; - } else if (eOSVersionId.startsWith('0.4')) { - // Elementary OS 0.4 Loki is binary compatible with Ubuntu 16.04 - return Platform.Ubuntu16; + return Runtime.Debian_8; + case 'galliumos': + if (distributionVersion.startsWith('2.0')) { + return Runtime.Ubuntu_16; } + break; default: - return Platform.Windows; + return Runtime.UnknownRuntime; } + + return Runtime.UnknownVersion; + } +} + +/** + * 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 { + // 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 { + return new Promise((resolve, reject) => { + fs.readFile(filePath, 'utf8', (error, data) => { + if (error) { + reject(error); + } else { + resolve(LinuxDistribution.FromReleaseInfo(data)); + } + }); + }); } - return Platform.Unknown; + 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 double quotes if necessary + 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); + } } + diff --git a/tasks/packagetasks.js b/tasks/packagetasks.js index fdcfc6ba7b..721a5d2581 100644 --- a/tasks/packagetasks.js +++ b/tasks/packagetasks.js @@ -53,32 +53,32 @@ gulp.task('package:online', () => { //Install vsce to be able to run this task: npm install -g vsce gulp.task('package:offline', () => { const platform = require('../out/src/models/platform'); - const Platform = platform.Platform; + const Runtime = platform.Runtime; var json = JSON.parse(fs.readFileSync('package.json')); var name = json.name; var version = json.version; var packageName = name + '-' + version; var packages = []; - packages.push({rid: 'win7-x64', platform: Platform.Windows}); - packages.push({rid: 'osx.10.11-x64', platform: Platform.OSX}); - packages.push({rid: 'centos.7-x64', platform: Platform.CentOS}); - packages.push({rid: 'debian.8-x64', platform: Platform.Debian}); - packages.push({rid: 'fedora.23-x64', platform: Platform.Fedora}); - packages.push({rid: 'opensuse.13.2-x64', platform:Platform.OpenSUSE}); - packages.push({rid: 'rhel.7.2-x64', platform: Platform.RHEL}); - packages.push({rid: 'ubuntu.14.04-x64', platform: Platform.Ubuntu14}); - packages.push({rid: 'ubuntu.16.04-x64', platform: Platform.Ubuntu16}); + packages.push({rid: 'win7-x64', runtime: Runtime.Windows_7_64}); + packages.push({rid: 'osx.10.11-x64', runtime: Runtime.OSX_10_11_64}); + packages.push({rid: 'centos.7-x64', runtime: Runtime.CentOS_7}); + packages.push({rid: 'debian.8-x64', runtime: Runtime.Debian_8}); + packages.push({rid: 'fedora.23-x64', runtime: Runtime.Fedora_23}); + packages.push({rid: 'opensuse.13.2-x64', runtime:Runtime.OpenSUSE_13_2}); + packages.push({rid: 'rhel.7.2-x64', runtime: Runtime.RHEL_7}); + packages.push({rid: 'ubuntu.14.04-x64', runtime: Runtime.Ubuntu_14}); + packages.push({rid: 'ubuntu.16.04-x64', runtime: Runtime.Ubuntu_16}); var promise = Promise.resolve(); cleanServiceInstallFolder().then(() => { packages.forEach(data => { promise = promise.then(() => { - return doOfflinePackage(data.rid, data.platform, packageName).then(() => { + return doOfflinePackage(data.rid, data.runtime, packageName).then(() => { return cleanServiceInstallFolder(); }); }); - }); + }); }); return promise; diff --git a/test/download.test.ts b/test/download.test.ts index 8ad58d6079..97e8a1f7b6 100644 --- a/test/download.test.ts +++ b/test/download.test.ts @@ -3,7 +3,7 @@ import * as TypeMoq from 'typemoq'; import {IConfig} from '../src/languageservice/interfaces'; import ServiceDownloadProvider from '../src/languageservice/download'; import Config from '../src/configurations/config'; -import {Platform} from '../src/models/platform'; +import * as Platform from '../src/models/platform'; import * as path from 'path'; suite('ServiceDownloadProvider Tests', () => { @@ -21,7 +21,7 @@ suite('ServiceDownloadProvider Tests', () => { config.setup(x => x.getSqlToolsInstallDirectory()).returns(() => expectedPathFromConfig); config.setup(x => x.getSqlToolsPackageVersion()).returns(() => expectedVersionFromConfig); let downloadProvider = new ServiceDownloadProvider(config.object, undefined); - let actual = downloadProvider.getInstallDirectory(Platform.OSX); + let actual = downloadProvider.getInstallDirectory(Platform.Runtime.OSX_10_11_64); assert.equal(expected, actual); done(); }); @@ -35,7 +35,7 @@ suite('ServiceDownloadProvider Tests', () => { config.setup(x => x.getSqlToolsInstallDirectory()).returns(() => expectedPathFromConfig); config.setup(x => x.getSqlToolsPackageVersion()).returns(() => expectedVersionFromConfig); let downloadProvider = new ServiceDownloadProvider(config.object, undefined); - let actual = downloadProvider.getInstallDirectory(Platform.OSX); + let actual = downloadProvider.getInstallDirectory(Platform.Runtime.OSX_10_11_64); assert.equal(expected, actual); done(); }); @@ -49,7 +49,7 @@ suite('ServiceDownloadProvider Tests', () => { config.setup(x => x.getSqlToolsInstallDirectory()).returns(() => expectedPathFromConfig); config.setup(x => x.getSqlToolsPackageVersion()).returns(() => expectedVersionFromConfig); let downloadProvider = new ServiceDownloadProvider(config.object, undefined); - let actual = downloadProvider.getInstallDirectory(Platform.OSX); + let actual = downloadProvider.getInstallDirectory(Platform.Runtime.OSX_10_11_64); assert.equal(expected, actual); done(); }); @@ -63,7 +63,7 @@ suite('ServiceDownloadProvider Tests', () => { config.setup(x => x.getSqlToolsInstallDirectory()).returns(() => expectedPathFromConfig); config.setup(x => x.getSqlToolsPackageVersion()).returns(() => expectedVersionFromConfig); let downloadProvider = new ServiceDownloadProvider(config.object, undefined); - let actual = downloadProvider.getInstallDirectory(Platform.OSX); + let actual = downloadProvider.getInstallDirectory(Platform.Runtime.OSX_10_11_64); assert.equal(expected, actual); done(); }); diff --git a/test/platform.test.ts b/test/platform.test.ts index 7feb595b8b..3e5bc03e58 100644 --- a/test/platform.test.ts +++ b/test/platform.test.ts @@ -1,11 +1,10 @@ import assert = require('assert'); -import {Platform, getCurrentPlatform} from '../src/models/platform'; +import {Runtime, PlatformInformation} from '../src/models/platform'; import Telemetry from '../src/models/telemetry'; -function getPlatform(): Promise { - return new Promise((resolve, reject) => { - let platform = getCurrentPlatform(); - resolve(platform); +function getPlatform(): Promise { + return PlatformInformation.GetCurrent().then (platformInfo => { + return platformInfo.runtimeId; }); } @@ -17,7 +16,7 @@ suite('Platform Tests', () => { test('getCurrentPlatform should return valid value', (done) => { getPlatform().then(platform => { - assert.notEqual(platform, Platform.Unknown); + assert.notEqual(platform, Runtime.UnknownRuntime); done(); }); }); diff --git a/test/server.test.ts b/test/server.test.ts index d408857823..3c50e2708b 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -6,7 +6,7 @@ import StatusView from './../src/views/statusView'; import Config from './../src/configurations/config'; import {ExtensionWrapper} from '../src/languageservice/extUtil'; import * as path from 'path'; -import {getCurrentPlatform} from '../src/models/platform'; +import {Runtime} from '../src/models/platform'; import {IConfig, IStatusView, IExtensionWrapper} from '../src/languageservice/interfaces'; suite('Server tests', () => { @@ -25,8 +25,9 @@ suite('Server tests', () => { test('findServerPath should return error given a folder with no installed service', () => { let installDir = __dirname; + const platform = Runtime.Windows_7_64; testConfig.setup(x => x.getSqlToolsExecutableFiles()).returns(() => ['exeFile1', 'exeFile2']); - testDownloadProvider.setup(x => x.getInstallDirectory()).returns(() => installDir); + testDownloadProvider.setup(x => x.getInstallDirectory(platform)).returns(() => installDir); testVsCode.setup(x => x.getActiveTextEditorUri()).returns(() => 'test'); let server = new ServerProvider(testDownloadProvider.object, testConfig.object, testStatusView.object, testVsCode.object); @@ -38,7 +39,8 @@ suite('Server tests', () => { test('findServerPath should return the file path given a file that exists', () => { let installDir = __dirname; let fileName = path.join(installDir, __filename); - testDownloadProvider.setup(x => x.getInstallDirectory()).returns(() => installDir); + const platform = Runtime.Windows_7_64; + testDownloadProvider.setup(x => x.getInstallDirectory(platform)).returns(() => installDir); testVsCode.setup(x => x.getActiveTextEditorUri()).returns(() => 'test'); let server = new ServerProvider(testDownloadProvider.object, testConfig.object, testStatusView.object, testVsCode.object); @@ -50,8 +52,9 @@ suite('Server tests', () => { test('findServerPath should not return the given file path if doesn not exist', () => { let installDir = __dirname; let fileName = path.join(installDir, __filename); + const platform = Runtime.Windows_7_64; testConfig.setup(x => x.getSqlToolsExecutableFiles()).returns(() => ['exeFile1', 'exeFile2']); - testDownloadProvider.setup(x => x.getInstallDirectory()).returns(() => installDir); + testDownloadProvider.setup(x => x.getInstallDirectory(platform)).returns(() => installDir); testVsCode.setup(x => x.getActiveTextEditorUri()).returns(() => 'test'); let server = new ServerProvider(testDownloadProvider.object, testConfig.object, testStatusView.object, testVsCode.object); @@ -63,8 +66,9 @@ suite('Server tests', () => { test('findServerPath should return a valid file path given a folder with installed service', () => { let installDir = __dirname; let fileName = __filename; + const platform = Runtime.Windows_7_64; testConfig.setup(x => x.getSqlToolsExecutableFiles()).returns(() => ['exeFile1', fileName]); - testDownloadProvider.setup(x => x.getInstallDirectory()).returns(() => installDir); + testDownloadProvider.setup(x => x.getInstallDirectory(platform)).returns(() => installDir); testVsCode.setup(x => x.getActiveTextEditorUri()).returns(() => 'test'); let server = new ServerProvider(testDownloadProvider.object, testConfig.object, testStatusView.object, testVsCode.object); @@ -76,7 +80,7 @@ suite('Server tests', () => { test('getServerPath should download the service if not exist and return the valid service file path', () => { let installDir = __dirname; let fileName: string = __filename.replace(installDir, ''); - const platform = getCurrentPlatform(); + const platform = Runtime.Windows_7_64; let executables: string[] = ['exeFile1']; testConfig.setup(x => x.getSqlToolsExecutableFiles()).returns(() => executables); @@ -99,7 +103,7 @@ suite('Server tests', () => { test('getServerPath should not download the service if already exist', () => { let installDir = __dirname; let fileName: string = __filename.replace(installDir, ''); - const platform = getCurrentPlatform(); + const platform = Runtime.Windows_7_64; let executables: string[] = [fileName]; testConfig.setup(x => x.getSqlToolsExecutableFiles()).returns(() => executables);