diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d8850b6..2e1100a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [2.20.0] - reduce hover verbosity to only title and description - If $/snyk.hasAuthenticated transmits an API URL, this is saved in the settings. +- Added CLI release channel. +- Added option to change base URL to download CLI. +- Run Snyk language Server from the CLI extension. +- Change default CLI download path to be in extension directory. - Delete sentry reporting. - send analytics event "plugin installed" the first time the extension is started diff --git a/package.json b/package.json index 00fa70147..697fb08da 100644 --- a/package.json +++ b/package.json @@ -303,17 +303,29 @@ "scope": "machine", "markdownDescription": "Snyk will download, install and update dependencies for you. If this option is disabled, make sure valid paths to the dependencies are provided." }, - "snyk.advanced.cliPath": { + "snyk.advanced.cliBaseDownloadUrl": { "order": 2, "type": "string", "scope": "machine", - "markdownDescription": "Sets path to Snyk CLI extension dependency." + "default": "https://downloads.snyk.io", + "markdownDescription": "Base URL to download the CLI." }, - "snyk.advanced.languageServerPath": { + "snyk.advanced.cliReleaseChannel": { "order": 3, "type": "string", + "default": "stable", + "enum": [ + "stable", + "rc", + "preview" + ], + "markdownDescription": "CLI release channel." + }, + "snyk.advanced.cliPath": { + "order": 4, + "type": "string", "scope": "machine", - "markdownDescription": "Sets path to Snyk Language Server (requires restart)." + "markdownDescription": "Sets path to Snyk CLI extension dependency." } } } diff --git a/src/snyk/base/modules/interfaces.ts b/src/snyk/base/modules/interfaces.ts index 92eb7d3c6..c67f3c283 100644 --- a/src/snyk/base/modules/interfaces.ts +++ b/src/snyk/base/modules/interfaces.ts @@ -1,5 +1,6 @@ import { IWorkspaceTrust } from '../../common/configuration/trustedFolders'; import { IContextService } from '../../common/services/contextService'; +import { DownloadService } from '../../common/services/downloadService'; import { IOpenerService } from '../../common/services/openerService'; import { IViewManagerService } from '../../common/services/viewManagerService'; import { ExtensionContext } from '../../common/vscode/extensionContext'; @@ -28,5 +29,7 @@ export interface ISnykLib { export interface IExtension extends IBaseSnykModule, ISnykLib { context: ExtensionContext | undefined; activate(context: VSCodeExtensionContext): void; + stopLanguageServer(): Promise; restartLanguageServer(): Promise; + initDependencyDownload(): DownloadService; } diff --git a/src/snyk/cli/cliExecutable.ts b/src/snyk/cli/cliExecutable.ts index bcb835900..3bfe289b5 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -1,37 +1,77 @@ -import * as fs from 'fs/promises'; import path from 'path'; -import { Platform } from '../common/platform'; -import { Checksum } from './checksum'; +import fs from 'fs/promises'; import { CliSupportedPlatform } from './supportedPlatforms'; +import { Checksum } from './checksum'; +import { Platform } from '../common/platform'; -// TODO: This file is to be removed in VS Code + Language Server feature cleanup. We need to ensure all users have migrated to use CLI path that's set by the language server. export class CliExecutable { - // If values updated, `.vscodeignore` to be changed. public static filenameSuffixes: Record = { linux: 'snyk-linux', - win32: 'snyk-win.exe', - darwin: 'snyk-macos', + linux_arm64: 'snyk-linux-arm64', + linux_alpine: 'snyk-alpine', + linux_alpine_arm64: 'snyk-alpine-arm64', + macos: 'snyk-macos', + macos_arm64: 'snyk-macos-arm64', + windows: 'snyk-win.exe', + windows_arm64: 'snyk-win.exe', }; - constructor(public readonly version: string, public readonly checksum: Checksum) {} - static getFilename(platform: CliSupportedPlatform): string { - return this.filenameSuffixes[platform]; - } - - static getPath(extensionDir: string, customPath?: string): string { + static async getPath(extensionDir: string, customPath?: string): Promise { if (customPath) { return customPath; } - const platform = Platform.getCurrent(); - const fileName = CliExecutable.getFilename(platform as CliSupportedPlatform); + const platform = await this.getCurrentWithArch(); + const fileName = this.getFileName(platform); return path.join(extensionDir, fileName); } - static exists(extensionDir: string, customPath?: string): Promise { + static getFileName(platform: CliSupportedPlatform): string { + return this.filenameSuffixes[platform]; + } + + static async getCurrentWithArch(): Promise { + const osName = Platform.getCurrent().toString().toLowerCase(); + const archSuffix = Platform.getArch().toLowerCase(); + const platform = await this.getPlatformName(osName); + + let cliName = platform; + if (archSuffix === 'arm64') { + cliName = `${platform}_${archSuffix}`; + } + return cliName as CliSupportedPlatform; + } + + static async getPlatformName(osName: string): Promise { + let platform = ''; + if (osName === 'linux') { + if (await this.isAlpine()) { + platform = 'linux_alpine'; + } else { + platform = 'linux'; + } + } else if (osName === 'darwin') { + platform = 'macos'; + } else if (osName === 'win32') { + platform = 'windows'; + } + if (!platform) { + throw new Error(`${osName} is unsupported.`); + } + return platform; + } + + static async exists(extensionDir: string, customPath?: string): Promise { + return fs + .access(await CliExecutable.getPath(extensionDir, customPath)) + .then(() => true) + .catch(() => false); + } + + static isAlpine(): Promise { return fs - .access(CliExecutable.getPath(extensionDir, customPath)) + .access('/etc/alpine-release') .then(() => true) .catch(() => false); } diff --git a/src/snyk/cli/staticCliApi.ts b/src/snyk/cli/staticCliApi.ts new file mode 100644 index 000000000..1fcb26847 --- /dev/null +++ b/src/snyk/cli/staticCliApi.ts @@ -0,0 +1,84 @@ +import axios, { CancelTokenSource } from 'axios'; +import { IConfiguration } from '../common/configuration/configuration'; +import { PROTOCOL_VERSION } from '../common/constants/languageServer'; +import { DownloadAxiosResponse } from '../common/download/downloader'; +import { ILog } from '../common/logger/interfaces'; +import { getAxiosConfig } from '../common/proxy'; +import { IVSCodeWorkspace } from '../common/vscode/workspace'; +import { CliExecutable } from './cliExecutable'; +import { CliSupportedPlatform } from './supportedPlatforms'; + +export interface IStaticCliApi { + getLatestCliVersion(releaseChannel: string): Promise; + downloadBinary(platform: CliSupportedPlatform): Promise<[Promise, CancelTokenSource]>; + getSha256Checksum(version: string, platform: CliSupportedPlatform): Promise; +} + +export class StaticCliApi implements IStaticCliApi { + constructor( + private readonly workspace: IVSCodeWorkspace, + private readonly configuration: IConfiguration, + private readonly logger: ILog, + ) {} + + getLatestVersionDownloadUrl(releaseChannel: string): string { + const downloadUrl = `${this.configuration.getCliBaseDownloadUrl()}/cli/${releaseChannel}/ls-protocol-version-${PROTOCOL_VERSION}`; + return downloadUrl; + } + + getDownloadUrl(version: string, platform: CliSupportedPlatform): string { + if (!version.startsWith('v')) { + version = `v${version}`; + } + const downloadUrl = `${this.configuration.getCliBaseDownloadUrl()}/cli/${version}/${this.getFileName(platform)}`; + return downloadUrl; + } + + getSha256DownloadUrl(version: string, platform: CliSupportedPlatform): string { + const downloadUrl = `${this.getDownloadUrl(version, platform)}.sha256`; + return downloadUrl; + } + + getFileName(platform: CliSupportedPlatform): string { + return CliExecutable.getFileName(platform); + } + + async getLatestCliVersion(releaseChannel: string): Promise { + let { data } = await axios.get( + this.getLatestVersionDownloadUrl(releaseChannel), + await getAxiosConfig(this.workspace, this.configuration, this.logger), + ); + data = data.replace('\n', ''); + if (data == '') return Promise.reject(new Error('CLI Version not found')); + return data; + } + + async downloadBinary(platform: CliSupportedPlatform): Promise<[Promise, CancelTokenSource]> { + const axiosCancelToken = axios.CancelToken.source(); + const latestCliVersion = await this.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + + const downloadUrl = this.getDownloadUrl(latestCliVersion, platform); + + const response = axios.get(downloadUrl, { + responseType: 'stream', + cancelToken: axiosCancelToken.token, + ...(await getAxiosConfig(this.workspace, this.configuration, this.logger)), + }); + + return [response as Promise, axiosCancelToken]; + } + + async getSha256Checksum(version: string, platform: CliSupportedPlatform): Promise { + const fileName = this.getFileName(platform); + const { data } = await axios.get( + `${this.getSha256DownloadUrl(version, platform)}`, + await getAxiosConfig(this.workspace, this.configuration, this.logger), + ); + + const checksum = data.replace(fileName, '').replace('\n', '').trim(); + + if (!checksum) return Promise.reject(new Error('Checksum not found')); + + return checksum; + } +} diff --git a/src/snyk/cli/supportedPlatforms.ts b/src/snyk/cli/supportedPlatforms.ts index 4fb3e84c1..96c85fe6a 100644 --- a/src/snyk/cli/supportedPlatforms.ts +++ b/src/snyk/cli/supportedPlatforms.ts @@ -1,2 +1,11 @@ -const SupportedCliPlatformsList = ['linux', 'win32', 'darwin'] as const; +const SupportedCliPlatformsList = [ + 'linux', + 'linux_arm64', + 'linux_alpine', + 'linux_alpine_arm64', + 'windows', + 'windows_arm64', + 'macos', + 'macos_arm64', +] as const; export type CliSupportedPlatform = typeof SupportedCliPlatformsList[number]; diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 066ebbb2f..3df7b4ad6 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -8,7 +8,9 @@ import { ADVANCED_AUTHENTICATION_METHOD, ADVANCED_AUTOMATIC_DEPENDENCY_MANAGEMENT, ADVANCED_AUTOSCAN_OSS_SETTING, + ADVANCED_CLI_BASE_DOWNLOAD_URL, ADVANCED_CLI_PATH, + ADVANCED_CLI_RELEASE_CHANNEL, ADVANCED_CUSTOM_ENDPOINT, ADVANCED_CUSTOM_LS_PATH, ADVANCED_ORGANIZATION, @@ -30,6 +32,8 @@ import { } from '../constants/settings'; import SecretStorageAdapter from '../vscode/secretStorage'; import { IVSCodeWorkspace } from '../vscode/workspace'; +import { CliExecutable } from '../../cli/cliExecutable'; +import { extensionContext } from '../vscode/extensionContext'; const NEWISSUES = 'Net new issues'; @@ -85,6 +89,9 @@ export interface IConfiguration { setCliPath(cliPath: string): Promise; + setCliReleaseChannel(releaseChannel: string): Promise; + setCliBaseDownloadUrl(baseDownloadUrl: string): Promise; + clearToken(): Promise; snykCodeUrl: string; @@ -113,8 +120,9 @@ export interface IConfiguration { isAutomaticDependencyManagementEnabled(): boolean; - getCliPath(): string | undefined; - + getCliPath(): Promise; + getCliReleaseChannel(): string; + getCliBaseDownloadUrl(): string; getInsecure(): boolean; isFedramp: boolean; @@ -145,15 +153,59 @@ export interface IConfiguration { export class Configuration implements IConfiguration { private readonly defaultAuthHost = 'https://app.snyk.io'; private readonly defaultApiEndpoint = 'https://api.snyk.io'; + private readonly defaultCliBaseDownloadUrl = 'https://downloads.snyk.io'; + private readonly defaultCliReleaseChannel = 'stable'; private featureFlag: { [key: string]: boolean } = {}; constructor(private processEnv: NodeJS.ProcessEnv = process.env, private workspace: IVSCodeWorkspace) {} + async setCliReleaseChannel(releaseChannel: string): Promise { + if (!releaseChannel) return; + return this.workspace.updateConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(ADVANCED_CLI_RELEASE_CHANNEL), + releaseChannel, + true, + ); + } + async setCliBaseDownloadUrl(baseDownloadUrl: string): Promise { + if (!baseDownloadUrl) return; + return this.workspace.updateConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(ADVANCED_CLI_BASE_DOWNLOAD_URL), + baseDownloadUrl, + true, + ); + } + + getCliReleaseChannel(): string { + return ( + this.workspace.getConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(ADVANCED_CLI_RELEASE_CHANNEL), + ) ?? this.defaultCliReleaseChannel + ); + } + getCliBaseDownloadUrl(): string { + return ( + this.workspace.getConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(ADVANCED_CLI_BASE_DOWNLOAD_URL), + ) ?? this.defaultCliBaseDownloadUrl + ); + } getOssQuickFixCodeActionsEnabled(): boolean { return this.getPreviewFeatures().ossQuickfixes ?? false; } + getSnykLanguageServerPath(): string | undefined { + return this.workspace.getConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(ADVANCED_CUSTOM_LS_PATH), + ); + } + getInsecure(): boolean { const strictSSL = this.workspace.getConfiguration('http', 'proxyStrictSSL') ?? true; return !strictSSL; @@ -239,13 +291,6 @@ export class Configuration implements IConfiguration { return `${authUrl.toString()}manage/snyk-code?from=vscode`; } - getSnykLanguageServerPath(): string | undefined { - return this.workspace.getConfiguration( - CONFIGURATION_IDENTIFIER, - this.getConfigName(ADVANCED_CUSTOM_LS_PATH), - ); - } - getDeltaFindingsEnabled(): boolean { const selectionValue = this.workspace.getConfiguration( CONFIGURATION_IDENTIFIER, @@ -285,7 +330,9 @@ export class Configuration implements IConfiguration { } async setCliPath(cliPath: string | undefined): Promise { - if (!cliPath) return; + if (!cliPath) { + cliPath = await CliExecutable.getPath(extensionContext.extensionPath); + } return this.workspace.updateConfiguration( CONFIGURATION_IDENTIFIER, this.getConfigName(ADVANCED_CLI_PATH), @@ -487,10 +534,28 @@ export class Configuration implements IConfiguration { ); } - getCliPath(): string | undefined { - return this.workspace.getConfiguration(CONFIGURATION_IDENTIFIER, this.getConfigName(ADVANCED_CLI_PATH)); + async getCliPath(): Promise { + let cliPath = this.workspace.getConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(ADVANCED_CLI_PATH), + ); + if (!cliPath) { + cliPath = await this.determineCliPath(); + await this.setCliPath(cliPath); + } + return cliPath; } + async determineCliPath(): Promise { + // if CLI Path is empty and Automatic Dependency management is disabled + // But Snyk-LS path is set, we will set CLI Path to Snyk LS path. + // This is a workaround that should be removed after the release of v2.20.0 + const isAutomaticDependencyManagementEnabled = this.isAutomaticDependencyManagementEnabled(); + const snykLsPath = this.getSnykLanguageServerPath(); + if (!isAutomaticDependencyManagementEnabled && snykLsPath) return snykLsPath; + const defaultPath = await CliExecutable.getPath(extensionContext.extensionPath); + return defaultPath; + } getTrustedFolders(): string[] { return ( this.workspace.getConfiguration(CONFIGURATION_IDENTIFIER, this.getConfigName(TRUSTED_FOLDERS)) || [] diff --git a/src/snyk/common/constants/globalState.ts b/src/snyk/common/constants/globalState.ts index 64b00fb10..4d8add9da 100644 --- a/src/snyk/common/constants/globalState.ts +++ b/src/snyk/common/constants/globalState.ts @@ -1,5 +1,5 @@ export const MEMENTO_ANONYMOUS_ID = 'snyk.anonymousId'; -export const MEMENTO_LS_LAST_UPDATE_DATE = 'snyk.lsLastUpdateDate'; +export const MEMENTO_CLI_VERSION = 'snyk.cliVersion'; +export const MEMENTO_CLI_CHECKSUM = 'snyk.cliChecksum'; export const MEMENTO_LS_PROTOCOL_VERSION = 'snyk.lsProtocolVersion'; -export const MEMENTO_LS_CHECKSUM = 'snyk.lsChecksum'; export const MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT = 'snyk.pluginInstalledSent'; diff --git a/src/snyk/common/constants/languageServer.ts b/src/snyk/common/constants/languageServer.ts index eb96907da..4ab1e3e12 100644 --- a/src/snyk/common/constants/languageServer.ts +++ b/src/snyk/common/constants/languageServer.ts @@ -9,7 +9,6 @@ export const DID_CHANGE_CONFIGURATION_METHOD = 'workspace/didChangeConfiguration // custom methods export const SNYK_HAS_AUTHENTICATED = '$/snyk.hasAuthenticated'; -export const SNYK_CLI_PATH = '$/snyk.isAvailableCli'; export const SNYK_ADD_TRUSTED_FOLDERS = '$/snyk.addTrustedFolders'; export const SNYK_SCAN = '$/snyk.scan'; export const SNYK_FOLDERCONFIG = '$/snyk.folderConfigs'; diff --git a/src/snyk/common/constants/settings.ts b/src/snyk/common/constants/settings.ts index 1e82f8c6a..f2f9e2d28 100644 --- a/src/snyk/common/constants/settings.ts +++ b/src/snyk/common/constants/settings.ts @@ -20,6 +20,8 @@ export const ADVANCED_ORGANIZATION = `${CONFIGURATION_IDENTIFIER}.advanced.organ export const ADVANCED_AUTOMATIC_DEPENDENCY_MANAGEMENT = `${CONFIGURATION_IDENTIFIER}.advanced.automaticDependencyManagement`; export const ADVANCED_CLI_PATH = `${CONFIGURATION_IDENTIFIER}.advanced.cliPath`; export const ADVANCED_CUSTOM_LS_PATH = `${CONFIGURATION_IDENTIFIER}.advanced.languageServerPath`; +export const ADVANCED_CLI_BASE_DOWNLOAD_URL = `${CONFIGURATION_IDENTIFIER}.advanced.cliBaseDownloadUrl`; +export const ADVANCED_CLI_RELEASE_CHANNEL = `${CONFIGURATION_IDENTIFIER}.advanced.cliReleaseChannel`; export const ADVANCED_AUTHENTICATION_METHOD = `${CONFIGURATION_IDENTIFIER}.advanced.authenticationMethod`; export const ISSUE_VIEW_OPTIONS_SETTING = `${CONFIGURATION_IDENTIFIER}.issueViewOptions`; diff --git a/src/snyk/common/download/downloader.ts b/src/snyk/common/download/downloader.ts index 0d02fe5e8..9a59abfa8 100644 --- a/src/snyk/common/download/downloader.ts +++ b/src/snyk/common/download/downloader.ts @@ -1,53 +1,51 @@ import axios, { CancelTokenSource } from 'axios'; import * as fs from 'fs'; -import { mkdirSync } from 'fs'; import * as fsPromises from 'fs/promises'; -import path from 'path'; import * as stream from 'stream'; import { Progress } from 'vscode'; import { Checksum } from '../../cli/checksum'; -import { CliExecutable } from '../../cli/cliExecutable'; import { messages } from '../../cli/messages/messages'; import { IConfiguration } from '../configuration/configuration'; -import { LsExecutable } from '../languageServer/lsExecutable'; -import { IStaticLsApi } from '../languageServer/staticLsApi'; -import { LsSupportedPlatform } from '../languageServer/supportedPlatforms'; +import { IStaticCliApi } from '../../cli/staticCliApi'; +import { CliExecutable } from '../../cli/cliExecutable'; import { ILog } from '../logger/interfaces'; import { CancellationToken } from '../vscode/types'; import { IVSCodeWindow } from '../vscode/window'; +import { CliSupportedPlatform } from '../../cli/supportedPlatforms'; +import { ExtensionContext } from '../vscode/extensionContext'; export type DownloadAxiosResponse = { data: stream.Readable; headers: { [header: string]: unknown } }; export class Downloader { constructor( private readonly configuration: IConfiguration, - private readonly lsApi: IStaticLsApi, + private readonly cliApi: IStaticCliApi, private readonly window: IVSCodeWindow, private readonly logger: ILog, + private readonly extensionContext: ExtensionContext, ) {} - /** - * Downloads LS. Existing executable is deleted. + * Downloads CLI. Existing executable is deleted. */ - async download(): Promise { - const lsPlatform = LsExecutable.getCurrentWithArch(); - if (lsPlatform === null) { + async download(): Promise { + const platform = await CliExecutable.getCurrentWithArch(); + if (platform === null) { return Promise.reject(!messages.notSupported); } - return await this.getLsExecutable(lsPlatform); + return await this.getCliExecutable(platform); } - private async getLsExecutable(lsPlatform: LsSupportedPlatform): Promise { - const lsPath = LsExecutable.getPath(this.configuration.getSnykLanguageServerPath()); - const lsDir = path.dirname(lsPath); - mkdirSync(lsDir, { recursive: true }); - if (await this.binaryExists(lsPath)) { - await this.deleteFileAtPath(lsPath); + private async getCliExecutable(platform: CliSupportedPlatform): Promise { + const cliPath = await CliExecutable.getPath( + this.extensionContext.extensionPath, + await this.configuration.getCliPath(), + ); + if (await this.binaryExists(cliPath)) { + await this.deleteFileAtPath(cliPath); } - - const lsVersion = (await this.lsApi.getMetadata()).version; - const sha256 = await this.lsApi.getSha256Checksum(lsPlatform); - const checksum = await this.downloadLs(lsPath, lsPlatform, sha256); + const cliVersion = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const sha256 = await this.cliApi.getSha256Checksum(cliVersion, platform); + const checksum = await this.downloadCli(cliPath, platform, sha256); if (!checksum) { return null; @@ -58,7 +56,7 @@ export class Downloader { return Promise.reject(messages.integrityCheckFailed); } - return new LsExecutable(lsVersion, checksum); + return new CliExecutable(cliVersion, checksum); } private async binaryExists(filePath: string): Promise { @@ -78,25 +76,25 @@ export class Downloader { } } - public async downloadLs( - lsPath: string, - platform: LsSupportedPlatform, + public async downloadCli( + cliPath: string, + platform: CliSupportedPlatform, expectedChecksum: string, ): Promise { const hash = new Checksum(expectedChecksum); return this.window.withProgress(messages.progressTitle, async (progress, token) => { const [request, requestToken]: [response: Promise, cancelToken: CancelTokenSource] = - await this.lsApi.downloadBinary(platform); + await this.cliApi.downloadBinary(platform); token.onCancellationRequested(async () => { requestToken.cancel(); this.logger.info(messages.downloadCanceled); - await this.deleteFileAtPath(lsPath); + await this.deleteFileAtPath(cliPath); }); progress.report({ increment: 0 }); - return await this.doDownload(requestToken, token, lsPath, request, hash, progress); + return await this.doDownload(requestToken, token, cliPath, request, hash, progress); }); } diff --git a/src/snyk/common/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index dc8dfa689..2dc10243b 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -4,7 +4,6 @@ import { IAuthenticationService } from '../../base/services/authenticationServic import { FolderConfig, IConfiguration } from '../configuration/configuration'; import { SNYK_ADD_TRUSTED_FOLDERS, - SNYK_CLI_PATH, SNYK_FOLDERCONFIG, SNYK_HAS_AUTHENTICATED, SNYK_LANGUAGE_SERVER_NAME, @@ -20,10 +19,11 @@ import { ILanguageClientAdapter } from '../vscode/languageClient'; import { LanguageClient, LanguageClientOptions, ServerOptions } from '../vscode/types'; import { IVSCodeWindow } from '../vscode/window'; import { IVSCodeWorkspace } from '../vscode/workspace'; -import { LsExecutable } from './lsExecutable'; +import { CliExecutable } from '../../cli/cliExecutable'; import { LanguageClientMiddleware } from './middleware'; import { LanguageServerSettings, ServerSettings } from './settings'; import { CodeIssueData, IacIssueData, OssIssueData, Scan } from './types'; +import { ExtensionContext } from '../vscode/extensionContext'; export interface ILanguageServer { start(): Promise; @@ -50,6 +50,7 @@ export class LanguageServer implements ILanguageServer { private authenticationService: IAuthenticationService, private readonly logger: ILog, private downloadService: DownloadService, + private extensionContext: ExtensionContext, ) { this.downloadService = downloadService; } @@ -77,7 +78,10 @@ export class LanguageServer implements ILanguageServer { }; } - const lsBinaryPath = LsExecutable.getPath(this.configuration.getSnykLanguageServerPath()); + const cliBinaryPath = await CliExecutable.getPath( + this.extensionContext.extensionPath, + await this.configuration.getCliPath(), + ); // log level is set to info by default let logLevel = 'info'; @@ -92,10 +96,10 @@ export class LanguageServer implements ILanguageServer { logLevel = process.env.SNYK_LOG_LEVEL ?? logLevel; const args = ['language-server', '-l', logLevel]; - this.logger.info(`Snyk Language Server - path: ${lsBinaryPath}`); + this.logger.info(`Snyk Language Server - path: ${cliBinaryPath}`); this.logger.info(`Snyk Language Server - args: ${args}`); const serverOptions: ServerOptions = { - command: lsBinaryPath, + command: cliBinaryPath, args: args, options: { env: processEnv, @@ -110,7 +114,7 @@ export class LanguageServer implements ILanguageServer { synchronize: { configurationSection: CONFIGURATION_IDENTIFIER, }, - middleware: new LanguageClientMiddleware(this.configuration, this.user), + middleware: new LanguageClientMiddleware(this.configuration, this.user, this.extensionContext), /** * We reuse the output channel here as it's not properly disposed of by the language client (vscode-languageclient@8.0.0-next.2) * See: https://github.com/microsoft/vscode-languageserver-node/blob/cdf4d6fdaefe329ce417621cf0f8b14e0b9bb39d/client/src/common/client.ts#L2789 @@ -145,30 +149,6 @@ export class LanguageServer implements ILanguageServer { }); }); - client.onNotification(SNYK_CLI_PATH, ({ cliPath }: { cliPath: string }) => { - if (!cliPath) { - ErrorHandler.handle( - new Error("CLI path wasn't provided by language server on $/snyk.isAvailableCli notification " + cliPath), - this.logger, - "CLI path wasn't provided by language server on notification", - ); - return; - } - - const currentCliPath = this.configuration.getCliPath(); - if (currentCliPath != cliPath) { - this.logger.info('Setting Snyk CLI path to: ' + cliPath); - void this.configuration - .setCliPath(cliPath) - .then(() => { - this.cliReady$.next(cliPath); - }) - .catch((error: Error) => { - ErrorHandler.handle(error, this.logger, error.message); - }); - } - }); - client.onNotification(SNYK_ADD_TRUSTED_FOLDERS, ({ trustedFolders }: { trustedFolders: string[] }) => { this.configuration.setTrustedFolders(trustedFolders).catch((error: Error) => { ErrorHandler.handle(error, this.logger, error.message); @@ -184,7 +164,11 @@ export class LanguageServer implements ILanguageServer { // Initialization options are not semantically equal to server settings, thus separated here // https://github.com/microsoft/language-server-protocol/issues/567 async getInitializationOptions(): Promise { - const settings = await LanguageServerSettings.fromConfiguration(this.configuration, this.user); + const settings = await LanguageServerSettings.fromConfiguration( + this.configuration, + this.user, + this.extensionContext, + ); return settings; } diff --git a/src/snyk/common/languageServer/lsExecutable.ts b/src/snyk/common/languageServer/lsExecutable.ts deleted file mode 100644 index 341e1e04a..000000000 --- a/src/snyk/common/languageServer/lsExecutable.ts +++ /dev/null @@ -1,80 +0,0 @@ -import os from 'os'; -import path from 'path'; -import { Checksum } from '../../cli/checksum'; -import { Platform } from '../platform'; -import { LsSupportedPlatform, SupportedLsPlatformsList } from './supportedPlatforms'; -import fs from 'fs/promises'; -import { IConfiguration } from '../configuration/configuration'; - -export class LsExecutable { - private static filenamePrefix = 'snyk-ls'; - public static filenameSuffixes: Record = { - linux386: 'linux_386', - linuxAmd64: 'linux_amd64', - linuxArm64: 'linux_arm64', - windows386: 'windows_386.exe', - windowsAmd64: 'windows_amd64.exe', - darwinAmd64: 'darwin_amd64', - darwinArm64: 'darwin_arm64', - }; - - public static defaultPaths: Record = { - linux386: process.env.XDG_DATA_HOME ?? '/.local/share/', - linuxAmd64: process.env.XDG_DATA_HOME ?? '/.local/share/', - linuxArm64: process.env.XDG_DATA_HOME ?? '/.local/share/', - windows386: process.env.XDG_DATA_HOME ?? '\\AppData\\Local\\', - windowsAmd64: process.env.XDG_DATA_HOME ?? '\\AppData\\Local\\', - darwinAmd64: process.env.XDG_DATA_HOME ?? '/Library/Application Support/', - darwinArm64: process.env.XDG_DATA_HOME ?? '/Library/Application Support/', - }; - - constructor(public readonly version: string, public readonly checksum: Checksum) {} - - static getFilename(platform: LsSupportedPlatform): string { - return `${this.filenamePrefix}_${this.filenameSuffixes[platform]}`; - } - - static getVersionedFilename(platform: LsSupportedPlatform, version: string) { - return `${this.filenamePrefix}_${version}_${this.filenameSuffixes[platform]}`; - } - - static getPath(customPath?: string): string { - if (customPath) { - return customPath; - } - - const platform = LsExecutable.getCurrentWithArch(); - - const homeDir = Platform.getHomeDir(); - const lsFilename = LsExecutable.getFilename(platform); - const defaultPath = this.defaultPaths[platform]; - const lsDir = path.join(homeDir, defaultPath, 'snyk-ls'); - return path.join(lsDir, lsFilename); - } - - static getCurrentWithArch(): LsSupportedPlatform { - let opSys = os.platform().toString(); - if (opSys === 'win32') { - opSys = 'windows'; - } - let opArch = os.arch(); - if (opArch === 'x64') { - opArch = 'amd64'; - } - if (opArch === 'ia32') { - opArch = '386'; - } - const supportPlatform = `${opSys}${opArch.charAt(0).toUpperCase()}${opArch.slice(1)}`; - if (SupportedLsPlatformsList.find(p => p === supportPlatform) !== undefined) { - return supportPlatform as LsSupportedPlatform; - } - throw new Error(`Unsupported platform: ${supportPlatform}`); - } - - static exists(configuration: IConfiguration): Promise { - return fs - .access(LsExecutable.getPath(configuration.getSnykLanguageServerPath())) - .then(() => true) - .catch(() => false); - } -} diff --git a/src/snyk/common/languageServer/middleware.ts b/src/snyk/common/languageServer/middleware.ts index ed0dd6385..8f6d4fe41 100644 --- a/src/snyk/common/languageServer/middleware.ts +++ b/src/snyk/common/languageServer/middleware.ts @@ -1,5 +1,6 @@ import { IConfiguration } from '../../common/configuration/configuration'; import { User } from '../user'; +import { ExtensionContext } from '../vscode/extensionContext'; import type { CancellationToken, ConfigurationParams, @@ -19,7 +20,7 @@ type LanguageClientWorkspaceMiddleware = Partial & { }; export class LanguageClientMiddleware implements Middleware { - constructor(private configuration: IConfiguration, private user: User) {} + constructor(private configuration: IConfiguration, private user: User, private extensionContext: ExtensionContext) {} workspace: LanguageClientWorkspaceMiddleware = { configuration: async ( @@ -40,7 +41,11 @@ export class LanguageClientMiddleware implements Middleware { return []; } - const serverSettings = await LanguageServerSettings.fromConfiguration(this.configuration, this.user); + const serverSettings = await LanguageServerSettings.fromConfiguration( + this.configuration, + this.user, + this.extensionContext, + ); return [serverSettings]; }, }; diff --git a/src/snyk/common/languageServer/settings.ts b/src/snyk/common/languageServer/settings.ts index 723bea05a..54a3be767 100644 --- a/src/snyk/common/languageServer/settings.ts +++ b/src/snyk/common/languageServer/settings.ts @@ -3,6 +3,8 @@ import { CLI_INTEGRATION_NAME } from '../../cli/contants/integration'; import { Configuration, FolderConfig, IConfiguration, SeverityFilter } from '../configuration/configuration'; import { User } from '../user'; import { PROTOCOL_VERSION } from '../constants/languageServer'; +import { CliExecutable } from '../../cli/cliExecutable'; +import { ExtensionContext } from '../vscode/extensionContext'; export type ServerSettings = { // Feature toggles @@ -48,7 +50,11 @@ export type ServerSettings = { }; export class LanguageServerSettings { - static async fromConfiguration(configuration: IConfiguration, user: User): Promise { + static async fromConfiguration( + configuration: IConfiguration, + user: User, + extensionContext: ExtensionContext, + ): Promise { const featuresConfiguration = configuration.getFeaturesConfiguration(); const ossEnabled = _.isUndefined(featuresConfiguration.ossEnabled) ? true : featuresConfiguration.ossEnabled; @@ -68,7 +74,7 @@ export class LanguageServerSettings { activateSnykIac: `${iacEnabled}`, enableDeltaFindings: `${configuration.getDeltaFindingsEnabled()}`, sendErrorReports: `${configuration.shouldReportErrors}`, - cliPath: configuration.getCliPath(), + cliPath: await CliExecutable.getPath(extensionContext.extensionPath, await configuration.getCliPath()), endpoint: configuration.snykApiEndpoint, organization: configuration.organization, token: await configuration.getToken(), diff --git a/src/snyk/common/languageServer/staticLsApi.ts b/src/snyk/common/languageServer/staticLsApi.ts deleted file mode 100644 index 9558b1ec5..000000000 --- a/src/snyk/common/languageServer/staticLsApi.ts +++ /dev/null @@ -1,90 +0,0 @@ -import axios, { CancelTokenSource } from 'axios'; -import { IConfiguration } from '../configuration/configuration'; -import { PROTOCOL_VERSION } from '../constants/languageServer'; -import { DownloadAxiosResponse } from '../download/downloader'; -import { ILog } from '../logger/interfaces'; -import { getAxiosConfig } from '../proxy'; -import { IVSCodeWorkspace } from '../vscode/workspace'; -import { LsExecutable } from './lsExecutable'; -import { LsSupportedPlatform } from './supportedPlatforms'; - -export type LsMetadata = { - tag: string; - version: string; - commit: string; - date: string; - previous_tag: string; - project_name: string; - runtime: string; -}; - -export interface IStaticLsApi { - getDownloadUrl(platform: LsSupportedPlatform): Promise; - - downloadBinary(platform: LsSupportedPlatform): Promise<[Promise, CancelTokenSource]>; - - getMetadata(): Promise; - - getSha256Checksum(platform: LsSupportedPlatform): Promise; -} - -export class StaticLsApi implements IStaticLsApi { - private readonly baseUrl = `https://downloads.snyk.io/snyk-ls/${PROTOCOL_VERSION}`; - - constructor( - private readonly workspace: IVSCodeWorkspace, - private readonly configuration: IConfiguration, - private readonly logger: ILog, - ) {} - - async getDownloadUrl(platform: LsSupportedPlatform): Promise { - return `${this.baseUrl}/${await this.getFileName(platform)}`; - } - - async getFileName(platform: LsSupportedPlatform): Promise { - return LsExecutable.getVersionedFilename(platform, await this.getLatestVersion()); - } - - async downloadBinary(platform: LsSupportedPlatform): Promise<[Promise, CancelTokenSource]> { - const axiosCancelToken = axios.CancelToken.source(); - const downloadUrl = await this.getDownloadUrl(platform); - - const response = axios.get(downloadUrl, { - responseType: 'stream', - cancelToken: axiosCancelToken.token, - ...(await getAxiosConfig(this.workspace, this.configuration, this.logger)), - }); - - return [response as Promise, axiosCancelToken]; - } - - async getLatestVersion(): Promise { - return Promise.resolve(this.getMetadata().then(metadata => metadata.version)); - } - - async getSha256Checksum(platform: LsSupportedPlatform): Promise { - const fileName = await this.getFileName(platform); - const { data } = await axios.get( - `${this.baseUrl}/snyk-ls_${await this.getLatestVersion()}_SHA256SUMS`, - await getAxiosConfig(this.workspace, this.configuration, this.logger), - ); - - let checksum = ''; - data.split('\n').forEach(line => { - if (line.includes(fileName)) { - checksum = line.split(' ')[0].trim().toLowerCase(); - } - }); - if (checksum == '') return Promise.reject(new Error('Checksum not found')); - - return checksum; - } - - async getMetadata(): Promise { - const response = await axios.get( - `${this.baseUrl}/metadata.json`, - await getAxiosConfig(this.workspace, this.configuration, this.logger), - ); - return response.data; - } -} diff --git a/src/snyk/common/languageServer/supportedPlatforms.ts b/src/snyk/common/languageServer/supportedPlatforms.ts deleted file mode 100644 index 7eaa14b6c..000000000 --- a/src/snyk/common/languageServer/supportedPlatforms.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const SupportedLsPlatformsList = [ - 'darwinAmd64', - 'darwinArm64', - 'linux386', - 'linuxAmd64', - 'linuxArm64', - 'windows386', - 'windowsAmd64', -] as const; - -export type LsSupportedPlatform = typeof SupportedLsPlatformsList[number]; diff --git a/src/snyk/common/platform.ts b/src/snyk/common/platform.ts index ecb9a09ca..8475f00c1 100644 --- a/src/snyk/common/platform.ts +++ b/src/snyk/common/platform.ts @@ -5,6 +5,10 @@ export class Platform { return os.platform(); } + static getArch(): string { + return os.arch(); + } + static getVersion(): string { return `${os.release()}-${os.arch}`; } diff --git a/src/snyk/common/services/downloadService.ts b/src/snyk/common/services/downloadService.ts index 8cf86d0cc..5834cae53 100644 --- a/src/snyk/common/services/downloadService.ts +++ b/src/snyk/common/services/downloadService.ts @@ -2,45 +2,39 @@ import { ReplaySubject } from 'rxjs'; import { Checksum } from '../../cli/checksum'; import { messages } from '../../cli/messages/messages'; import { IConfiguration } from '../configuration/configuration'; -import { - MEMENTO_LS_CHECKSUM, - MEMENTO_LS_LAST_UPDATE_DATE, - MEMENTO_LS_PROTOCOL_VERSION, -} from '../constants/globalState'; -import { PROTOCOL_VERSION } from '../constants/languageServer'; +import { MEMENTO_CLI_CHECKSUM, MEMENTO_CLI_VERSION, MEMENTO_LS_PROTOCOL_VERSION } from '../constants/globalState'; import { Downloader } from '../download/downloader'; -import { LsExecutable } from '../languageServer/lsExecutable'; -import { IStaticLsApi } from '../languageServer/staticLsApi'; -import { LsSupportedPlatform } from '../languageServer/supportedPlatforms'; +import { CliExecutable } from '../../cli/cliExecutable'; +import { IStaticCliApi } from '../../cli/staticCliApi'; import { ILog } from '../logger/interfaces'; import { ExtensionContext } from '../vscode/extensionContext'; import { IVSCodeWindow } from '../vscode/window'; +import { CliSupportedPlatform } from '../../cli/supportedPlatforms'; +import { PROTOCOL_VERSION } from '../constants/languageServer'; export class DownloadService { - readonly fourDaysInMs = 4 * 24 * 3600 * 1000; readonly downloadReady$ = new ReplaySubject(1); - private readonly downloader: Downloader; constructor( private readonly extensionContext: ExtensionContext, private readonly configuration: IConfiguration, - private readonly lsApi: IStaticLsApi, + private readonly cliApi: IStaticCliApi, readonly window: IVSCodeWindow, private readonly logger: ILog, downloader?: Downloader, ) { - this.downloader = downloader ?? new Downloader(configuration, lsApi, window, logger); + this.downloader = downloader ?? new Downloader(configuration, cliApi, window, logger, this.extensionContext); } async downloadOrUpdate(): Promise { - const lsInstalled = await this.isLsInstalled(); + const cliInstalled = await this.isCliInstalled(); if (!this.configuration.isAutomaticDependencyManagementEnabled()) { this.downloadReady$.next(); return false; } - if (!lsInstalled) { + if (!cliInstalled) { const downloaded = await this.download(); this.downloadReady$.next(); return downloaded; @@ -59,20 +53,20 @@ export class DownloadService { return false; } - await this.setLastLsUpdateDateAndChecksum(executable.checksum); - await this.setCurrentLspVersion(); + await this.setCliChecksum(executable.checksum); + await this.setCliVersion(executable.version); this.logger.info(messages.downloadFinished(executable.version)); return true; } async update(): Promise { - // let language server manage CLI downloads, but download LS here - const platform = LsExecutable.getCurrentWithArch(); - const lsInstalled = await this.isLsInstalled(); - const lspVersionHasUpdated = this.hasLspVersionUpdated(); - const needsUpdate = this.isFourDaysPassedSinceLastLsUpdate() || lspVersionHasUpdated; - if (!lsInstalled || needsUpdate) { - const updateAvailable = await this.isLsUpdateAvailable(platform); + const platform = await CliExecutable.getCurrentWithArch(); + const version = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const cliInstalled = await this.isCliInstalled(); + const cliVersionHasUpdated = this.hasCliVersionUpdated(version); + const needsUpdate = cliVersionHasUpdated || this.hasLspVersionUpdated(); + if (!cliInstalled || needsUpdate) { + const updateAvailable = await this.isCliUpdateAvailable(platform); if (!updateAvailable) { return false; } @@ -81,7 +75,8 @@ export class DownloadService { return false; } - await this.setLastLsUpdateDateAndChecksum(executable.checksum); + await this.setCliChecksum(executable.checksum); + await this.setCliVersion(executable.version); await this.setCurrentLspVersion(); this.logger.info(messages.downloadFinished(executable.version)); return true; @@ -89,17 +84,23 @@ export class DownloadService { return false; } - async isLsInstalled() { - const lsExecutableExists = await LsExecutable.exists(this.configuration); - const lastUpdateDateWritten = !!this.getLastLsUpdateDate(); - const lsChecksumWritten = !!this.getLsChecksum(); + async isCliInstalled() { + const cliExecutableExists = await CliExecutable.exists( + this.extensionContext.extensionPath, + await this.configuration.getCliPath(), + ); + const cliChecksumWritten = !!this.getCliChecksum(); - return lsExecutableExists && lastUpdateDateWritten && lsChecksumWritten; + return cliExecutableExists && cliChecksumWritten; } - private async isLsUpdateAvailable(platform: LsSupportedPlatform): Promise { - const latestChecksum = await this.lsApi.getSha256Checksum(platform); - const path = LsExecutable.getPath(this.configuration.getSnykLanguageServerPath()); + private async isCliUpdateAvailable(platform: CliSupportedPlatform): Promise { + const version = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const latestChecksum = await this.cliApi.getSha256Checksum(version, platform); + const path = await CliExecutable.getPath( + this.extensionContext.extensionPath, + await this.configuration.getCliPath(), + ); // Update is available if fetched checksum not matching the current one const checksum = await Checksum.getChecksumOf(path, latestChecksum); @@ -111,21 +112,12 @@ export class DownloadService { return true; } - private async setLastLsUpdateDateAndChecksum(checksum: Checksum): Promise { - await this.extensionContext.updateGlobalStateValue(MEMENTO_LS_LAST_UPDATE_DATE, Date.now()); - await this.extensionContext.updateGlobalStateValue(MEMENTO_LS_CHECKSUM, checksum.checksum); - } - - private async setCurrentLspVersion(): Promise { - await this.extensionContext.updateGlobalStateValue(MEMENTO_LS_PROTOCOL_VERSION, PROTOCOL_VERSION); + private async setCliChecksum(checksum: Checksum): Promise { + await this.extensionContext.updateGlobalStateValue(MEMENTO_CLI_CHECKSUM, checksum.checksum); } - private isFourDaysPassedSinceLastLsUpdate(): boolean { - const lastUpdateDate = this.getLastLsUpdateDate(); - if (!lastUpdateDate) { - throw new Error('Last update date is not known.'); - } - return Date.now() - lastUpdateDate > this.fourDaysInMs; + private async setCliVersion(cliVersion: string): Promise { + await this.extensionContext.updateGlobalStateValue(MEMENTO_CLI_VERSION, cliVersion); } private hasLspVersionUpdated(): boolean { @@ -133,15 +125,24 @@ export class DownloadService { return currentProtoclVersion != PROTOCOL_VERSION; } - private getLastLsUpdateDate() { - return this.extensionContext.getGlobalStateValue(MEMENTO_LS_LAST_UPDATE_DATE); + private async setCurrentLspVersion(): Promise { + await this.extensionContext.updateGlobalStateValue(MEMENTO_LS_PROTOCOL_VERSION, PROTOCOL_VERSION); } private getLsProtocolVersion() { return this.extensionContext.getGlobalStateValue(MEMENTO_LS_PROTOCOL_VERSION); } - private getLsChecksum(): number | undefined { - return this.extensionContext.getGlobalStateValue(MEMENTO_LS_CHECKSUM); + private hasCliVersionUpdated(cliVersion: string): boolean { + const currentVersion = this.getCliVersion(); + return currentVersion != cliVersion; + } + + private getCliVersion(): string | undefined { + return this.extensionContext.getGlobalStateValue(MEMENTO_CLI_VERSION); + } + + private getCliChecksum(): string | undefined { + return this.extensionContext.getGlobalStateValue(MEMENTO_CLI_CHECKSUM); } } diff --git a/src/snyk/common/watchers/configurationWatcher.ts b/src/snyk/common/watchers/configurationWatcher.ts index 174a242e1..c3846daaa 100644 --- a/src/snyk/common/watchers/configurationWatcher.ts +++ b/src/snyk/common/watchers/configurationWatcher.ts @@ -7,7 +7,6 @@ import { ADVANCED_ADVANCED_MODE_SETTING, ADVANCED_AUTOSCAN_OSS_SETTING, ADVANCED_CUSTOM_ENDPOINT, - ADVANCED_CUSTOM_LS_PATH, CODE_QUALITY_ENABLED_SETTING, CODE_SECURITY_ENABLED_SETTING, IAC_ENABLED_SETTING, @@ -19,6 +18,8 @@ import { DELTA_FINDINGS, FOLDER_CONFIGS, ADVANCED_AUTHENTICATION_METHOD, + ADVANCED_CLI_PATH, + ADVANCED_CLI_RELEASE_CHANNEL, } from '../constants/settings'; import { ErrorHandler } from '../error/errorHandler'; import { ILog } from '../logger/interfaces'; @@ -51,9 +52,13 @@ class ConfigurationWatcher implements IWatcher { await extension.contextService.setContext(SNYK_CONTEXT.LOGGEDIN, false); await extension.contextService.setContext(SNYK_CONTEXT.AUTHENTICATION_METHOD_CHANGED, true); return extension.viewManagerService.refreshAllViews(); - } else if (key === ADVANCED_CUSTOM_LS_PATH) { + } else if (key === ADVANCED_CLI_PATH) { // Language Server client must sync config changes before we can restart return _.debounce(() => extension.restartLanguageServer(), DEFAULT_LS_DEBOUNCE_INTERVAL)(); + } else if (key === ADVANCED_CLI_RELEASE_CHANNEL) { + await extension.stopLanguageServer(); + extension.initDependencyDownload(); + return; } else if (key === FOLDER_CONFIGS || key == DELTA_FINDINGS) { extension.viewManagerService.refreshAllViews(); } else if (key === TRUSTED_FOLDERS) { @@ -86,7 +91,8 @@ class ConfigurationWatcher implements IWatcher { IAC_ENABLED_SETTING, SEVERITY_FILTER_SETTING, ADVANCED_CUSTOM_ENDPOINT, - ADVANCED_CUSTOM_LS_PATH, + ADVANCED_CLI_PATH, + ADVANCED_CLI_RELEASE_CHANNEL, ADVANCED_AUTHENTICATION_METHOD, TRUSTED_FOLDERS, ISSUE_VIEW_OPTIONS_SETTING, diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index d4fedb230..e54d7654c 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -39,7 +39,8 @@ import { } from './common/constants/views'; import { ErrorHandler } from './common/error/errorHandler'; import { ExperimentService } from './common/experiment/services/experimentService'; -import { StaticLsApi } from './common/languageServer/staticLsApi'; +import { LanguageServer } from './common/languageServer/languageServer'; +import { StaticCliApi } from './cli/staticCliApi'; import { Logger } from './common/logger/logger'; import { DownloadService } from './common/services/downloadService'; import { LearnService } from './common/services/learnService'; @@ -82,13 +83,11 @@ import { GitAPI, GitExtension, Repository } from './common/git'; import { AnalyticsSender } from './common/analytics/AnalyticsSender'; import { MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT } from './common/constants/globalState'; import { AnalyticsEvent } from './common/analytics/AnalyticsEvent'; -import { LanguageServer } from './common/languageServer/languageServer'; class SnykExtension extends SnykLib implements IExtension { public async activate(vscodeContext: vscode.ExtensionContext): Promise { extensionContext.setContext(vscodeContext); this.context = extensionContext; - const snykConfiguration = await this.getSnykConfiguration(); try { @@ -183,7 +182,7 @@ class SnykExtension extends SnykLib implements IExtension { this.downloadService = new DownloadService( this.context, configuration, - new StaticLsApi(vsCodeWorkspace, configuration, Logger), + new StaticCliApi(vsCodeWorkspace, configuration, Logger), vsCodeWindow, Logger, ); @@ -199,6 +198,7 @@ class SnykExtension extends SnykLib implements IExtension { this.authService, Logger, this.downloadService, + this.context, ); const codeSuggestionProvider = new CodeSuggestionWebviewProvider( @@ -430,6 +430,10 @@ class SnykExtension extends SnykLib implements IExtension { this.featureFlagService = new FeatureFlagService(vsCodeCommands); await this.setupFeatureFlags(); + // Fetch feature flag to determine whether to use the new LSP-based rendering. + + // initialize contexts + await this.contextService.setContext(SNYK_CONTEXT.INITIALIZED, true); this.sendPluginInstalledEvent(); // Actually start analysis @@ -458,12 +462,16 @@ class SnykExtension extends SnykLib implements IExtension { await this.languageServer.stop(); } + public async stopLanguageServer(): Promise { + await this.languageServer.stop(); + } + public async restartLanguageServer(): Promise { await this.languageServer.stop(); await this.languageServer.start(); } - private initDependencyDownload(): DownloadService { + public initDependencyDownload(): DownloadService { this.downloadService.downloadOrUpdate().catch(err => { void ErrorHandler.handleGlobal(err, Logger, this.contextService, this.loadingBadge); }); diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index ceb8d6f27..c23a946d2 100644 --- a/src/test/unit/cli/cliExecutable.test.ts +++ b/src/test/unit/cli/cliExecutable.test.ts @@ -1,5 +1,6 @@ import { strictEqual } from 'assert'; import path from 'path'; +import fs from 'fs/promises'; import sinon from 'sinon'; import { CliExecutable } from '../../../snyk/cli/cliExecutable'; import { Platform } from '../../../snyk/common/platform'; @@ -10,30 +11,60 @@ suite('CliExecutable', () => { }); test('Returns correct filename for different platforms', () => { - strictEqual(CliExecutable.getFilename('linux'), 'snyk-linux'); - strictEqual(CliExecutable.getFilename('darwin'), 'snyk-macos'); - strictEqual(CliExecutable.getFilename('win32'), 'snyk-win.exe'); + strictEqual(CliExecutable.getFileName('linux'), 'snyk-linux'); + strictEqual(CliExecutable.getFileName('linux_alpine'), 'snyk-alpine'); + strictEqual(CliExecutable.getFileName('macos'), 'snyk-macos'); + strictEqual(CliExecutable.getFileName('macos_arm64'), 'snyk-macos-arm64'); + strictEqual(CliExecutable.getFileName('windows'), 'snyk-win.exe'); }); - test('Returns correct extension paths', () => { + test('Returns correct extension paths', async () => { const unixExtensionDir = '/Users/user/.vscode/extensions/snyk-security.snyk-vulnerability-scanner-1.1.0'; + const winExtensionDir = `C:\\Users\\user\\.vscode\\extensions`; + + const osStub = sinon.stub(Platform, 'getCurrent').returns('darwin'); + const archStub = sinon.stub(Platform, 'getArch').returns('x64'); + const fsStub = sinon.stub(fs, 'access').returns(Promise.reject()); - const stub = sinon.stub(Platform, 'getCurrent').returns('darwin'); let expectedCliPath = path.join(unixExtensionDir, 'snyk-macos'); - strictEqual(CliExecutable.getPath(unixExtensionDir), expectedCliPath); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); - stub.returns('linux'); + osStub.returns('linux'); expectedCliPath = path.join(unixExtensionDir, 'snyk-linux'); - strictEqual(CliExecutable.getPath(unixExtensionDir), expectedCliPath); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); - const winExtensionDir = `C:\\Users\\user\\.vscode\\extensions`; - stub.returns('win32'); + fsStub.returns(Promise.resolve()); + expectedCliPath = path.join(unixExtensionDir, 'snyk-alpine'); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + fsStub.returns(Promise.reject()); + + osStub.returns('win32'); + expectedCliPath = path.join(winExtensionDir, 'snyk-win.exe'); + strictEqual(await CliExecutable.getPath(winExtensionDir), expectedCliPath); + + // test arm64 + archStub.returns('arm64'); + + osStub.returns('darwin'); + expectedCliPath = path.join(unixExtensionDir, 'snyk-macos-arm64'); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + + osStub.returns('linux'); + expectedCliPath = path.join(unixExtensionDir, 'snyk-linux-arm64'); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + + fsStub.returns(Promise.resolve()); + expectedCliPath = path.join(unixExtensionDir, 'snyk-alpine-arm64'); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + fsStub.returns(Promise.reject()); + + osStub.returns('win32'); expectedCliPath = path.join(winExtensionDir, 'snyk-win.exe'); - strictEqual(CliExecutable.getPath(winExtensionDir), expectedCliPath); + strictEqual(await CliExecutable.getPath(winExtensionDir), expectedCliPath); }); - test('Return custom path, if provided', () => { + test('Return custom path, if provided', async () => { const customPath = '/path/to/cli'; - strictEqual(CliExecutable.getPath('', customPath), customPath); + strictEqual(await CliExecutable.getPath('', customPath), customPath); }); }); diff --git a/src/test/unit/common/configuration.test.ts b/src/test/unit/common/configuration.test.ts index 3600123a3..bfbe7c7b7 100644 --- a/src/test/unit/common/configuration.test.ts +++ b/src/test/unit/common/configuration.test.ts @@ -3,30 +3,30 @@ import { deepStrictEqual, strictEqual } from 'assert'; import sinon from 'sinon'; import { Configuration, PreviewFeatures } from '../../../snyk/common/configuration/configuration'; -import { SNYK_TOKEN_KEY } from '../../../snyk/common/constants/general'; import { + ADVANCED_CLI_PATH, ADVANCED_CUSTOM_ENDPOINT, + ADVANCED_CUSTOM_LS_PATH, FEATURES_PREVIEW_SETTING, SCANNING_MODE, } from '../../../snyk/common/constants/settings'; import SecretStorageAdapter from '../../../snyk/common/vscode/secretStorage'; -import { ExtensionContext } from '../../../snyk/common/vscode/types'; import { IVSCodeWorkspace } from '../../../snyk/common/vscode/workspace'; import { extensionContextMock } from '../mocks/extensionContext.mock'; import { stubWorkspaceConfiguration } from '../mocks/workspace.mock'; +import { extensionContext } from '../../../snyk/common/vscode/extensionContext'; +import { Platform } from '../../../snyk/common/platform'; +import path from 'path'; suite('Configuration', () => { let workspaceStub: IVSCodeWorkspace; - let extensionContext: ExtensionContext; setup(() => { const tokenConfigSection = 'token'; let token = ''; - - extensionContext = extensionContextMock; - SecretStorageAdapter.init(extensionContext); - + SecretStorageAdapter.init(extensionContextMock); + extensionContext.setContext(extensionContextMock); const stub = sinon.stub().returns({ getConfiguration(_configurationIdentifier, _section) { if (_section === tokenConfigSection) return token; @@ -138,5 +138,36 @@ suite('Configuration', () => { strictEqual(configuration.isFedramp, false); }); + + test('CLI Path: Returns default path if empty', async () => { + const workspace = stubWorkspaceConfiguration(ADVANCED_CLI_PATH, ''); + + const configuration = new Configuration({}, workspace); + sinon.stub(Platform, 'getCurrent').returns('linux'); + sinon.stub(Platform, 'getArch').returns('x64'); + + const cliPath = await configuration.getCliPath(); + + const expectedCliPath = path.join('path/to/extension/', 'snyk-linux'); + strictEqual(cliPath, expectedCliPath); + }); + + test('CLI Path: Returns Snyk LS path', async () => { + const workspace = stubWorkspaceConfiguration(ADVANCED_CUSTOM_LS_PATH, '/path/to/ls'); + + const configuration = new Configuration({}, workspace); + + const cliPath = await configuration.getCliPath(); + strictEqual(cliPath, '/path/to/ls'); + }); + + test('CLI Path: Returns CLI Path if set', async () => { + const workspace = stubWorkspaceConfiguration(ADVANCED_CLI_PATH, '/path/to/cli'); + + const configuration = new Configuration({}, workspace); + + const cliPath = await configuration.getCliPath(); + strictEqual(cliPath, '/path/to/cli'); + }); }); }); diff --git a/src/test/unit/common/languageServer/languageServer.test.ts b/src/test/unit/common/languageServer/languageServer.test.ts index 11449b01f..ddce3a8c5 100644 --- a/src/test/unit/common/languageServer/languageServer.test.ts +++ b/src/test/unit/common/languageServer/languageServer.test.ts @@ -17,6 +17,7 @@ import { LoggerMock } from '../../mocks/logger.mock'; import { windowMock } from '../../mocks/window.mock'; import { stubWorkspaceConfiguration } from '../../mocks/workspace.mock'; import { PROTOCOL_VERSION } from '../../../../snyk/common/constants/languageServer'; +import { ExtensionContext } from '../../../../snyk/common/vscode/extensionContext'; suite('Language Server', () => { const authServiceMock = {} as IAuthenticationService; @@ -25,6 +26,7 @@ suite('Language Server', () => { let configurationMock: IConfiguration; let languageServer: LanguageServer; let downloadServiceMock: DownloadService; + let extensionContextMock: ExtensionContext; const path = 'testPath'; const logger = { info(_msg: string) {}, @@ -35,6 +37,8 @@ suite('Language Server', () => { }, } as unknown as LoggerMock; + let contextGetGlobalStateValue: sinon.SinonStub; + setup(() => { configurationMock = { getAuthenticationMethod(): string { @@ -46,16 +50,13 @@ suite('Language Server', () => { getDeltaFindingsEnabled(): boolean { return false; }, - getCliPath(): string | undefined { - return path; + getCliPath(): Promise { + return Promise.resolve(path); }, getToken(): Promise { return Promise.resolve('testToken'); }, shouldReportErrors: true, - getSnykLanguageServerPath(): string { - return path; - }, getAdditionalCliParameters() { return '--all-projects -d'; }, @@ -86,6 +87,16 @@ suite('Language Server', () => { scanningMode: 'auto', } as IConfiguration; + extensionContextMock = { + extensionPath: 'test/path', + getGlobalStateValue: contextGetGlobalStateValue, + updateGlobalStateValue: sinon.fake(), + setContext: sinon.fake(), + subscriptions: [], + addDisposables: sinon.fake(), + getExtensionUri: sinon.fake(), + } as unknown as ExtensionContext; + downloadServiceMock = { downloadReady$: new ReplaySubject(1), } as DownloadService; @@ -130,6 +141,7 @@ suite('Language Server', () => { authServiceMock, logger, downloadServiceMock, + extensionContextMock, ); downloadServiceMock.downloadReady$.next(); @@ -179,6 +191,7 @@ suite('Language Server', () => { authServiceMock, new LoggerMock(), downloadServiceMock, + extensionContextMock, ); downloadServiceMock.downloadReady$.next(); await languageServer.start(); @@ -204,6 +217,7 @@ suite('Language Server', () => { authServiceMock, new LoggerMock(), downloadServiceMock, + extensionContextMock, ); }); @@ -250,6 +264,7 @@ suite('Language Server', () => { authServiceMock, new LoggerMock(), downloadServiceMock, + extensionContextMock, ); const initOptions = await languageServer.getInitializationOptions(); diff --git a/src/test/unit/common/languageServer/lsExecutable.test.ts b/src/test/unit/common/languageServer/lsExecutable.test.ts deleted file mode 100644 index 186bdfc53..000000000 --- a/src/test/unit/common/languageServer/lsExecutable.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { strictEqual } from 'assert'; -import os from 'os'; -import path from 'path'; -import sinon from 'sinon'; -import { LsExecutable } from '../../../../snyk/common/languageServer/lsExecutable'; -import { LsSupportedPlatform } from '../../../../snyk/common/languageServer/supportedPlatforms'; - -suite('LsExecutable', () => { - teardown(() => { - sinon.restore(); - }); - - test('Returns correct filename for different platforms', () => { - strictEqual(LsExecutable.getFilename('darwinAmd64'), 'snyk-ls_darwin_amd64'); - strictEqual(LsExecutable.getFilename('darwinArm64'), 'snyk-ls_darwin_arm64'); - strictEqual(LsExecutable.getFilename('linux386'), 'snyk-ls_linux_386'); - strictEqual(LsExecutable.getFilename('linuxAmd64'), 'snyk-ls_linux_amd64'); - strictEqual(LsExecutable.getFilename('linuxArm64'), 'snyk-ls_linux_arm64'); - strictEqual(LsExecutable.getFilename('windows386'), 'snyk-ls_windows_386.exe'); - strictEqual(LsExecutable.getFilename('windowsAmd64'), 'snyk-ls_windows_amd64.exe'); - }); - - test('Returns correct versioned filename for different platforms', () => { - const version = '20220101.101010'; - strictEqual(LsExecutable.getVersionedFilename('darwinAmd64', version), `snyk-ls_${version}_darwin_amd64`); - strictEqual(LsExecutable.getVersionedFilename('darwinArm64', version), `snyk-ls_${version}_darwin_arm64`); - strictEqual(LsExecutable.getVersionedFilename('linux386', version), `snyk-ls_${version}_linux_386`); - strictEqual(LsExecutable.getVersionedFilename('linuxAmd64', version), `snyk-ls_${version}_linux_amd64`); - strictEqual(LsExecutable.getVersionedFilename('linuxArm64', version), `snyk-ls_${version}_linux_arm64`); - strictEqual(LsExecutable.getVersionedFilename('windows386', version), `snyk-ls_${version}_windows_386.exe`); - strictEqual(LsExecutable.getVersionedFilename('windowsAmd64', version), `snyk-ls_${version}_windows_amd64.exe`); - }); - - test('Returns correct paths', () => { - const homedirStub = sinon.stub(os, 'homedir'); - const getCurrentWithArchStub = sinon.stub(LsExecutable, 'getCurrentWithArch'); - - // DarwinAmd64 - let macOSPlatform: LsSupportedPlatform = 'darwinAmd64'; - let homedir = '/Users/user'; - getCurrentWithArchStub.returns(macOSPlatform); - homedirStub.returns(homedir); - - let expectedFilename = LsExecutable.getFilename(macOSPlatform); - let expectedCliPath = path.join(homedir, '/Library/Application Support/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); - - // DarwinArm64 - macOSPlatform = 'darwinArm64'; - getCurrentWithArchStub.returns(macOSPlatform); - - expectedFilename = LsExecutable.getFilename(macOSPlatform); - expectedCliPath = path.join(homedir, '/Library/Application Support/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); - - // Linux386 - let linuxPlatform: LsSupportedPlatform = 'linux386'; - homedir = '/home/user'; - getCurrentWithArchStub.returns(linuxPlatform); - homedirStub.returns(homedir); - - expectedFilename = LsExecutable.getFilename(linuxPlatform); - expectedCliPath = path.join(homedir, '/.local/share/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); - - // LinuxAmd64 - linuxPlatform = 'linuxAmd64'; - getCurrentWithArchStub.returns(linuxPlatform); - - expectedFilename = LsExecutable.getFilename(linuxPlatform); - expectedCliPath = path.join(homedir, '/.local/share/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); - - // LinuxArm64 - linuxPlatform = 'linuxArm64'; - getCurrentWithArchStub.returns(linuxPlatform); - - expectedFilename = LsExecutable.getFilename(linuxPlatform); - expectedCliPath = path.join(homedir, '/.local/share/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); - - // Windows386 - let windowsPlatform: LsSupportedPlatform = 'windows386'; - homedir = 'C:\\Users\\user'; - getCurrentWithArchStub.returns(windowsPlatform); - homedirStub.returns(homedir); - - expectedFilename = LsExecutable.getFilename(windowsPlatform); - expectedCliPath = path.join(homedir, '\\AppData\\Local\\', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); - - // WindowsAmd64 - windowsPlatform = 'windowsAmd64'; - getCurrentWithArchStub.returns(windowsPlatform); - - expectedFilename = LsExecutable.getFilename(windowsPlatform); - expectedCliPath = path.join(homedir, '\\AppData\\Local\\', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); - }); - - test('Return custom path, if provided', () => { - const customPath = '/path/to/cli'; - strictEqual(LsExecutable.getPath(customPath), customPath); - }); - - test('Returns correct platform architecture', () => { - const platformStub = sinon.stub(os, 'platform'); - const archStub = sinon.stub(os, 'arch'); - - // OSX - platformStub.returns('darwin'); - archStub.returns('x64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'darwinAmd64'); - - archStub.returns('arm64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'darwinArm64'); - - // Linux - platformStub.returns('linux'); - archStub.returns('x64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'linuxAmd64'); - - archStub.returns('arm64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'linuxArm64'); - - archStub.returns('ia32'); - strictEqual(LsExecutable.getCurrentWithArch(), 'linux386'); - - // Windows - platformStub.returns('win32'); - archStub.returns('x64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'windowsAmd64'); - - archStub.returns('ia32'); - strictEqual(LsExecutable.getCurrentWithArch(), 'windows386'); - }); -}); diff --git a/src/test/unit/common/languageServer/middleware.test.ts b/src/test/unit/common/languageServer/middleware.test.ts index f15244631..2dbbd054e 100644 --- a/src/test/unit/common/languageServer/middleware.test.ts +++ b/src/test/unit/common/languageServer/middleware.test.ts @@ -12,11 +12,13 @@ import type { ResponseError, } from '../../../../snyk/common/vscode/types'; import { defaultFeaturesConfigurationStub } from '../../mocks/configuration.mock'; -import { extensionContextMock } from '../../mocks/extensionContext.mock'; +import { ExtensionContext } from '../../../../snyk/common/vscode/extensionContext'; suite('Language Server: Middleware', () => { let configuration: IConfiguration; let user: User; + let extensionContextMock: ExtensionContext; + let contextGetGlobalStateValue: sinon.SinonStub; setup(() => { user = { anonymousId: 'anonymous-id' } as User; @@ -30,7 +32,7 @@ suite('Language Server: Middleware', () => { organization: 'org', getToken: () => Promise.resolve('token'), isAutomaticDependencyManagementEnabled: () => true, - getCliPath: () => '/path/to/cli', + getCliPath: (): Promise => Promise.resolve('/path/to/cli'), getInsecure(): boolean { return true; }, @@ -57,6 +59,15 @@ suite('Language Server: Middleware', () => { return []; }, } as IConfiguration; + extensionContextMock = { + extensionPath: 'test/path', + getGlobalStateValue: contextGetGlobalStateValue, + updateGlobalStateValue: sinon.fake(), + setContext: sinon.fake(), + subscriptions: [], + addDisposables: sinon.fake(), + getExtensionUri: sinon.fake(), + } as unknown as ExtensionContext; }); teardown(() => { @@ -64,7 +75,7 @@ suite('Language Server: Middleware', () => { }); test('Configuration request should translate settings', async () => { - const middleware = new LanguageClientMiddleware(configuration, user); + const middleware = new LanguageClientMiddleware(configuration, user, extensionContextMock); const params: ConfigurationParams = { items: [ { @@ -101,14 +112,14 @@ suite('Language Server: Middleware', () => { ); assert.strictEqual( serverResult.cliPath, - CliExecutable.getPath(extensionContextMock.extensionPath, configuration.getCliPath()), + await CliExecutable.getPath(extensionContextMock.extensionPath, await configuration.getCliPath()), ); assert.strictEqual(serverResult.enableTrustedFoldersFeature, 'true'); assert.deepStrictEqual(serverResult.trustedFolders, configuration.getTrustedFolders()); }); test('Configuration request should return an error', async () => { - const middleware = new LanguageClientMiddleware(configuration, user); + const middleware = new LanguageClientMiddleware(configuration, user, extensionContextMock); const params: ConfigurationParams = { items: [ { diff --git a/src/test/unit/common/languageServer/settings.test.ts b/src/test/unit/common/languageServer/settings.test.ts index 5d5800d9c..dfc82d8f5 100644 --- a/src/test/unit/common/languageServer/settings.test.ts +++ b/src/test/unit/common/languageServer/settings.test.ts @@ -2,11 +2,21 @@ import assert from 'assert'; import { FolderConfig, IConfiguration, PreviewFeatures } from '../../../../snyk/common/configuration/configuration'; import { LanguageServerSettings } from '../../../../snyk/common/languageServer/settings'; import { User } from '../../../../snyk/common/user'; +import sinon from 'sinon'; +import { ExtensionContext } from '../../../../snyk/common/vscode/extensionContext'; suite('LanguageServerSettings', () => { suite('fromConfiguration', () => { test('should generate server settings with default true values for undefined feature toggles', async () => { const mockUser = { anonymousId: 'anonymous-id' } as User; + const extensionContextMock: ExtensionContext = { + extensionPath: 'test/path', + updateGlobalStateValue: sinon.fake(), + setContext: sinon.fake(), + subscriptions: [], + addDisposables: sinon.fake(), + getExtensionUri: sinon.fake(), + } as unknown as ExtensionContext; const mockConfiguration: IConfiguration = { shouldReportErrors: false, snykApiEndpoint: 'https://dev.snyk.io/api', @@ -34,9 +44,13 @@ suite('LanguageServerSettings', () => { }, severityFilter: { critical: true, high: true, medium: true, low: false }, scanningMode: 'scan-mode', - } as IConfiguration; + } as unknown as IConfiguration; - const serverSettings = await LanguageServerSettings.fromConfiguration(mockConfiguration, mockUser); + const serverSettings = await LanguageServerSettings.fromConfiguration( + mockConfiguration, + mockUser, + extensionContextMock, + ); assert.strictEqual(serverSettings.activateSnykCodeSecurity, 'true'); assert.strictEqual(serverSettings.activateSnykCodeQuality, 'true'); diff --git a/src/test/unit/common/services/downloadService.test.ts b/src/test/unit/common/services/downloadService.test.ts index 864867989..4d7526ec1 100644 --- a/src/test/unit/common/services/downloadService.test.ts +++ b/src/test/unit/common/services/downloadService.test.ts @@ -1,17 +1,10 @@ import { strictEqual } from 'assert'; import sinon, { stub } from 'sinon'; import { Checksum } from '../../../../snyk/cli/checksum'; -import { CliExecutable } from '../../../../snyk/cli/cliExecutable'; import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; -import { - MEMENTO_LS_CHECKSUM, - MEMENTO_LS_LAST_UPDATE_DATE, - MEMENTO_LS_PROTOCOL_VERSION, -} from '../../../../snyk/common/constants/globalState'; -import { PROTOCOL_VERSION } from '../../../../snyk/common/constants/languageServer'; import { Downloader } from '../../../../snyk/common/download/downloader'; -import { LsExecutable } from '../../../../snyk/common/languageServer/lsExecutable'; -import { IStaticLsApi } from '../../../../snyk/common/languageServer/staticLsApi'; +import { CliExecutable } from '../../../../snyk/cli/cliExecutable'; +import { IStaticCliApi } from '../../../../snyk/cli/staticCliApi'; import { ILog } from '../../../../snyk/common/logger/interfaces'; import { Platform } from '../../../../snyk/common/platform'; import { DownloadService } from '../../../../snyk/common/services/downloadService'; @@ -21,7 +14,7 @@ import { windowMock } from '../../mocks/window.mock'; suite('DownloadService', () => { let logger: ILog; - let lsApi: IStaticLsApi; + let cliApi: IStaticCliApi; let context: ExtensionContext; let downloader: Downloader; let configuration: IConfiguration; @@ -34,10 +27,9 @@ suite('DownloadService', () => { contextGetGlobalStateValue = sinon.stub(); apigetSha256Checksum = sinon.stub(); - lsApi = { - getDownloadUrl: sinon.fake(), + cliApi = { + getLatestCliVersion: sinon.fake(), downloadBinary: sinon.fake(), - getMetadata: sinon.fake(), getSha256Checksum: apigetSha256Checksum, }; @@ -55,24 +47,24 @@ suite('DownloadService', () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'ab/c', - } as unknown as IConfiguration; + getCliReleaseChannel: () => 'stable', + getCliPath: () => Promise.resolve('path/to/cli'), + } as IConfiguration; - downloader = new Downloader(configuration, lsApi, windowMock, logger); + downloader = new Downloader(configuration, cliApi, windowMock, logger, context); }); teardown(() => { sinon.restore(); }); - test('Tries to download LS if not installed', async () => { + test('Tries to download CLI if not installed', async () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'abc/d', - } as unknown as IConfiguration; - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); + getCliReleaseChannel: () => 'stable', + getCliPath: () => Promise.resolve('path/to/cli'), + } as IConfiguration; + const service = new DownloadService(context, configuration, cliApi, windowMock, logger, downloader); const downloadSpy = stub(service, 'download'); const updateSpy = stub(service, 'update'); await service.downloadOrUpdate(); @@ -81,14 +73,14 @@ suite('DownloadService', () => { strictEqual(updateSpy.called, false); }); - test('Tries to update LS if installed', async () => { + test('Tries to update CLI if installed', async () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'abc/d', - } as unknown as IConfiguration; - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); - stub(service, 'isLsInstalled').resolves(true); + getCliReleaseChannel: () => 'stable', + getCliPath: () => Promise.resolve('path/to/cli'), + } as IConfiguration; + const service = new DownloadService(context, configuration, cliApi, windowMock, logger, downloader); + stub(service, 'isCliInstalled').resolves(true); const downloadSpy = stub(service, 'download'); const updateSpy = stub(service, 'update'); @@ -98,112 +90,14 @@ suite('DownloadService', () => { strictEqual(updateSpy.calledOnce, true); }); - test('Updates LS if >4 days passed since last update and new version available', async () => { - configuration = { - isAutomaticDependencyManagementEnabled: () => true, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'abc/d', - } as unknown as IConfiguration; - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); - stub(service, 'isLsInstalled').resolves(true); - - const fiveDaysInMs = 5 * 24 * 3600 * 1000; - - contextGetGlobalStateValue.withArgs(MEMENTO_LS_LAST_UPDATE_DATE).returns(Date.now() - fiveDaysInMs); - - stubSuccessDownload(apigetSha256Checksum, downloader); - - const updated = await service.update(); - - strictEqual(updated, true); - }); - - test("Doesn't update LS if >4 days passed since last update but no new version available", async () => { - configuration = { - isAutomaticDependencyManagementEnabled: () => true, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'abc/d', - } as unknown as IConfiguration; - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); - stub(service, 'isLsInstalled').resolves(true); - - const fiveDaysInMs = 5 * 24 * 3600 * 1000; - - contextGetGlobalStateValue.withArgs(MEMENTO_LS_LAST_UPDATE_DATE).returns(Date.now() - fiveDaysInMs); - - const curChecksumStr = 'ba6b3c08ce5b9067ecda4f410e3b6c2662e01c064490994555f57b1cc25840f9'; - const latestChecksumStr = 'ba6b3c08ce5b9067ecda4f410e3b6c2662e01c064490994555f57b1cc25840f9'; - const latestChecksum = Checksum.fromDigest(curChecksumStr, latestChecksumStr); - apigetSha256Checksum.returns(latestChecksumStr); - - sinon.stub(Platform, 'getCurrent').returns('darwin'); - sinon.stub(Checksum, 'getChecksumOf').resolves(latestChecksum); - sinon.stub(downloader, 'download').resolves(new LsExecutable('1.0.0', new Checksum(latestChecksumStr))); - - const updated = await service.update(); - - strictEqual(updated, false); - }); - - test("Doesn't update LS if 3 days passed since last update and LSP version is latest", async () => { - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); - stub(service, 'isLsInstalled').resolves(true); - - const threeDaysInMs = 3 * 24 * 3600 * 1000; - contextGetGlobalStateValue.withArgs(MEMENTO_LS_LAST_UPDATE_DATE).returns(Date.now() - threeDaysInMs); - contextGetGlobalStateValue.withArgs(MEMENTO_LS_PROTOCOL_VERSION).returns(PROTOCOL_VERSION); - - sinon.stub(downloader, 'download').resolves(new LsExecutable('1.0.0', new Checksum('test'))); - - const updated = await service.update(); - - strictEqual(updated, false); - }); - - test('Updates if 3 days passed since last update and LSP version has increased', async () => { - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); - stub(service, 'isLsInstalled').resolves(true); - - const threeDaysInMs = 3 * 24 * 3600 * 1000; - contextGetGlobalStateValue.withArgs(MEMENTO_LS_LAST_UPDATE_DATE).returns(Date.now() - threeDaysInMs); - contextGetGlobalStateValue.withArgs(MEMENTO_LS_PROTOCOL_VERSION).returns(PROTOCOL_VERSION - 1); - - stubSuccessDownload(apigetSha256Checksum, downloader); - - const updated = await service.update(); - - strictEqual(updated, true); - }); - - test("Doesn't try to update if last LS update date was not set", async () => { - configuration = { - isAutomaticDependencyManagementEnabled: () => true, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'abc/d', - } as unknown as IConfiguration; - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); - contextGetGlobalStateValue.withArgs(MEMENTO_LS_CHECKSUM).returns(undefined); - contextGetGlobalStateValue.withArgs(MEMENTO_LS_LAST_UPDATE_DATE).returns(undefined); - - stub(CliExecutable, 'exists').resolves(true); - - const downloadSpy = stub(service, 'download'); - const updateSpy = stub(service, 'update'); - - await service.downloadOrUpdate(); - - strictEqual(downloadSpy.called, true); - strictEqual(updateSpy.calledOnce, false); - }); - - test("Doesn't download LS if automatic dependency management disabled", async () => { + test("Doesn't download CLI if automatic dependency management disabled", async () => { configuration = { isAutomaticDependencyManagementEnabled: () => false, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'abc/d', - } as unknown as IConfiguration; - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); - stub(service, 'isLsInstalled').resolves(false); + getCliReleaseChannel: () => 'stable', + getCliPath: () => Promise.resolve('path/to/cli'), + } as IConfiguration; + const service = new DownloadService(context, configuration, cliApi, windowMock, logger, downloader); + stub(service, 'isCliInstalled').resolves(false); const downloadSpy = stub(service, 'download'); const updateSpy = stub(service, 'update'); @@ -223,5 +117,5 @@ function stubSuccessDownload(apigetSha256Checksum: sinon.SinonStub, downloader: sinon.stub(Platform, 'getCurrent').returns('darwin'); sinon.stub(Checksum, 'getChecksumOf').resolves(latestChecksum); - sinon.stub(downloader, 'download').resolves(new LsExecutable('1.0.1', new Checksum(latestChecksumStr))); + sinon.stub(downloader, 'download').resolves(new CliExecutable('1.0.1', new Checksum(latestChecksumStr))); } diff --git a/src/test/unit/download/downloader.test.ts b/src/test/unit/download/downloader.test.ts index 136630947..4674b31e2 100644 --- a/src/test/unit/download/downloader.test.ts +++ b/src/test/unit/download/downloader.test.ts @@ -4,43 +4,40 @@ import sinon from 'sinon'; import { Checksum } from '../../../snyk/cli/checksum'; import { IConfiguration } from '../../../snyk/common/configuration/configuration'; import { Downloader } from '../../../snyk/common/download/downloader'; -import { LsExecutable } from '../../../snyk/common/languageServer/lsExecutable'; -import { IStaticLsApi, LsMetadata } from '../../../snyk/common/languageServer/staticLsApi'; +import { CliExecutable } from '../../../snyk/cli/cliExecutable'; +import { IStaticCliApi } from '../../../snyk/cli/staticCliApi'; import { ILog } from '../../../snyk/common/logger/interfaces'; import { LoggerMock } from '../mocks/logger.mock'; import { windowMock } from '../mocks/window.mock'; +import { ExtensionContext } from '../../../snyk/common/vscode/extensionContext'; -suite('LS Downloader (LS)', () => { +suite('CLI Downloader (CLI)', () => { let logger: ILog; - let lsApi: IStaticLsApi; + let cliApi: IStaticCliApi; let configuration: IConfiguration; - + let extensionContextMock: ExtensionContext; setup(() => { - lsApi = { - getDownloadUrl: sinon.fake(), + cliApi = { + getLatestCliVersion: sinon.fake(), downloadBinary: sinon.fake(), - getMetadata(): Promise { - return Promise.resolve({ - commit: 'abc', - date: '01.01.2001', - // eslint-disable-next-line camelcase - previous_tag: '', - // eslint-disable-next-line camelcase - project_name: 'testProject', - runtime: 'darwin', - tag: 'v20010101.010101', - version: 'v20010101.010101', - }); - }, getSha256Checksum: sinon.fake(), }; logger = new LoggerMock(); configuration = { isAutomaticDependencyManagementEnabled: () => true, - getSnykLanguageServerPath(): string { - return 'abc/d'; + getCliReleaseChannel: () => 'stable', + getCliPath(): Promise { + return Promise.resolve('abc/d'); }, } as IConfiguration; + extensionContextMock = { + extensionPath: 'test/path', + updateGlobalStateValue: sinon.fake(), + setContext: sinon.fake(), + subscriptions: [], + addDisposables: sinon.fake(), + getExtensionUri: sinon.fake(), + } as unknown as ExtensionContext; }); // noinspection DuplicatedCode @@ -48,31 +45,30 @@ suite('LS Downloader (LS)', () => { sinon.restore(); }); - test('Download of LS fails if platform is not supported', async () => { - const downloader = new Downloader(configuration, lsApi, windowMock, logger); - sinon.stub(LsExecutable, 'getCurrentWithArch').throws(new Error()); + test('Download of CLI fails if platform is not supported', async () => { + const downloader = new Downloader(configuration, cliApi, windowMock, logger, extensionContextMock); + sinon.stub(CliExecutable, 'getCurrentWithArch').throws(new Error()); await rejects(() => downloader.download()); }); - test('Download of LS removes executable, if it exists', async () => { - const downloader = new Downloader(configuration, lsApi, windowMock, logger); + test('Download of CLI removes executable, if it exists', async () => { + const downloader = new Downloader(configuration, cliApi, windowMock, logger, extensionContextMock); - sinon.stub(LsExecutable, 'getCurrentWithArch').returns('darwinArm64'); + sinon.stub(CliExecutable, 'getCurrentWithArch').resolves('macos_arm64'); sinon.stub(fs, 'access').returns(Promise.resolve()); const unlink = sinon.stub(fs, 'unlink'); await downloader.download(); - const lsPath = LsExecutable.getPath(configuration.getSnykLanguageServerPath()); - - strictEqual(unlink.calledOnceWith(lsPath), true); + const cliPath = (await configuration.getCliPath()) ?? ''; + strictEqual(unlink.calledOnceWith(cliPath), true); }); - test('Rejects downloaded LS when integrity check fails', async () => { - const downloader = new Downloader(configuration, lsApi, windowMock, logger); - sinon.stub(LsExecutable, 'getCurrentWithArch').returns('darwinAmd64'); + test('Rejects downloaded CLI when integrity check fails', async () => { + const downloader = new Downloader(configuration, cliApi, windowMock, logger, extensionContextMock); + sinon.stub(CliExecutable, 'getCurrentWithArch').resolves('macos_arm64'); sinon.stub(fs); - sinon.stub(downloader, 'downloadLs').resolves(new Checksum('test')); + sinon.stub(downloader, 'downloadCli').resolves(new Checksum('test')); await rejects(() => downloader.download()); }); diff --git a/src/test/unit/mocks/extensionContext.mock.ts b/src/test/unit/mocks/extensionContext.mock.ts index 4cc92a339..9d5bc5d99 100644 --- a/src/test/unit/mocks/extensionContext.mock.ts +++ b/src/test/unit/mocks/extensionContext.mock.ts @@ -4,6 +4,6 @@ export const extensionContextMock = { secrets: { store: (_key: string, _value: string) => Promise.resolve(), get: () => Promise.resolve(), - delete: () => Promise.resolve(), }, + extensionPath: 'path/to/extension', } as unknown as ExtensionContext; diff --git a/src/test/unit/mocks/workspace.mock.ts b/src/test/unit/mocks/workspace.mock.ts index 2e1ee2329..a32acf384 100644 --- a/src/test/unit/mocks/workspace.mock.ts +++ b/src/test/unit/mocks/workspace.mock.ts @@ -6,5 +6,8 @@ export function stubWorkspaceConfiguration(configSetting: string, returnValue if (`${identifier}.${key}` === configSetting) return returnValue; return undefined; }, + updateConfiguration(_configurationIdentifier, _section, _value, _configurationTarget, _overrideInLanguage) { + return Promise.resolve(); + }, } as IVSCodeWorkspace; }