From f894d9029741506941177ad9b5ab03ed7f7151f5 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Mon, 28 Oct 2024 17:50:04 +0100 Subject: [PATCH 01/23] feat: wip use cli ls extension --- package.json | 20 +++- src/snyk/base/modules/interfaces.ts | 3 + src/snyk/cli/cliExecutable.ts | 66 +++++++++---- src/snyk/cli/staticCliApi.ts | 84 ++++++++++++++++ src/snyk/cli/supportedPlatforms.ts | 2 +- .../common/configuration/configuration.ts | 83 ++++++++++++---- src/snyk/common/constants/globalState.ts | 5 +- src/snyk/common/constants/settings.ts | 3 +- src/snyk/common/download/downloader.ts | 61 +++++------- .../common/languageServer/languageServer.ts | 18 ++-- .../common/languageServer/lsExecutable.ts | 80 --------------- src/snyk/common/languageServer/middleware.ts | 5 +- src/snyk/common/languageServer/settings.ts | 6 +- src/snyk/common/languageServer/staticLsApi.ts | 90 ----------------- .../languageServer/supportedPlatforms.ts | 11 --- src/snyk/common/services/downloadService.ts | 97 ++++++++----------- .../common/watchers/configurationWatcher.ts | 12 ++- src/snyk/extension.ts | 19 ++-- .../languageServer/lsExecutable.test.ts | 75 ++++++-------- .../common/services/downloadService.test.ts | 22 ++--- src/test/unit/download/downloader.test.ts | 16 +-- 21 files changed, 377 insertions(+), 401 deletions(-) create mode 100644 src/snyk/cli/staticCliApi.ts delete mode 100644 src/snyk/common/languageServer/lsExecutable.ts delete mode 100644 src/snyk/common/languageServer/staticLsApi.ts delete mode 100644 src/snyk/common/languageServer/supportedPlatforms.ts diff --git a/package.json b/package.json index f34b00ae2..a29140043 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..6d3e3234e 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -1,37 +1,69 @@ -import * as fs from 'fs/promises'; +import os from 'os'; 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'; -// 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_alpine: 'snyk-alpine', + macos: 'snyk-macos', + macos_arm64: 'snyk-macos-arm64', + windows: '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 { + let platform = ''; + let osName = os.platform().toString().toLowerCase(); + let archName = os.arch().toLowerCase(); + if (osName === 'linux') { + if (await this.isAlpine()) { + platform = 'linux_alpine'; + } else { + platform = 'linux'; + } + } else if (osName === 'darwin') { + if (archName === 'arm64') { + platform = 'macos_arm64'; + } else { + platform = 'macos'; + } + } else if (osName.includes('win')) { + platform = 'windows'; + } + if (!platform) { + throw new Error(`${osName} is unsupported.`); + } + + return platform as CliSupportedPlatform; + } + + 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..6487a684a --- /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 = await 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..3cef834a9 100644 --- a/src/snyk/cli/supportedPlatforms.ts +++ b/src/snyk/cli/supportedPlatforms.ts @@ -1,2 +1,2 @@ -const SupportedCliPlatformsList = ['linux', 'win32', 'darwin'] as const; +const SupportedCliPlatformsList = ['linux', 'linux_alpine', 'windows', '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 15b8aeddc..743a272f0 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -8,9 +8,10 @@ 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, CODE_QUALITY_ENABLED_SETTING, CODE_SECURITY_ENABLED_SETTING, @@ -30,6 +31,8 @@ import { } from '../constants/settings'; import SecretStorageAdapter from '../vscode/secretStorage'; import { IVSCodeWorkspace } from '../vscode/workspace'; +import { CliExecutable } from '../../cli/cliExecutable'; +import SnykExtension from '../../extension'; const NEWISSUES = 'Net new issues'; @@ -85,6 +88,9 @@ export interface IConfiguration { setCliPath(cliPath: string): Promise; + setCliReleaseChannel(releaseChannel: string): Promise; + setCliBaseDownloadUrl(baseDownloadUrl: string): Promise; + clearToken(): Promise; snykCodeUrl: string; @@ -113,8 +119,9 @@ export interface IConfiguration { isAutomaticDependencyManagementEnabled(): boolean; - getCliPath(): string | undefined; - + getCliPath(): Promise; + getCliReleaseChannel(): string; + getCliBaseDownloadUrl(): string; getInsecure(): boolean; isFedramp: boolean; @@ -125,8 +132,6 @@ export interface IConfiguration { scanningMode: string | undefined; - getSnykLanguageServerPath(): string | undefined; - getTrustedFolders(): string[]; setTrustedFolders(trustedFolders: string[]): Promise; @@ -144,13 +149,52 @@ export interface IConfiguration { export class Configuration implements IConfiguration { // These attributes are used in tests - private readonly defaultSnykCodeBaseURL = 'https://deeproxy.snyk.io'; 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) {} + 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; @@ -241,13 +285,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, @@ -287,7 +324,10 @@ export class Configuration implements IConfiguration { } async setCliPath(cliPath: string | undefined): Promise { - if (!cliPath) return; + const extensionContext = SnykExtension.getExtensionContext(); + if (!cliPath && extensionContext) { + cliPath = await CliExecutable.getPath(extensionContext.extensionPath); + } return this.workspace.updateConfiguration( CONFIGURATION_IDENTIFIER, this.getConfigName(ADVANCED_CLI_PATH), @@ -489,8 +529,17 @@ 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), + ); + const extensionContext = SnykExtension.getExtensionContext(); + if (!cliPath && extensionContext) { + cliPath = await CliExecutable.getPath(SnykExtension.getExtensionContext().extensionPath); + this.setCliPath(cliPath); + } + return cliPath; } getTrustedFolders(): string[] { diff --git a/src/snyk/common/constants/globalState.ts b/src/snyk/common/constants/globalState.ts index ff12e3a06..2a83beec3 100644 --- a/src/snyk/common/constants/globalState.ts +++ b/src/snyk/common/constants/globalState.ts @@ -1,4 +1,3 @@ export const MEMENTO_ANONYMOUS_ID = 'snyk.anonymousId'; -export const MEMENTO_LS_LAST_UPDATE_DATE = 'snyk.lsLastUpdateDate'; -export const MEMENTO_LS_PROTOCOL_VERSION = 'snyk.lsProtocolVersion'; -export const MEMENTO_LS_CHECKSUM = 'snyk.lsChecksum'; +export const MEMENTO_CLI_VERSION = 'snyk.cliVersion'; +export const MEMENTO_CLI_CHECKSUM = 'snyk.cliChecksum'; diff --git a/src/snyk/common/constants/settings.ts b/src/snyk/common/constants/settings.ts index 1e82f8c6a..040d1b504 100644 --- a/src/snyk/common/constants/settings.ts +++ b/src/snyk/common/constants/settings.ts @@ -19,7 +19,8 @@ export const ADVANCED_CUSTOM_ENDPOINT = `${CONFIGURATION_IDENTIFIER}.advanced.cu export const ADVANCED_ORGANIZATION = `${CONFIGURATION_IDENTIFIER}.advanced.organization`; 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..97d060a63 100644 --- a/src/snyk/common/download/downloader.ts +++ b/src/snyk/common/download/downloader.ts @@ -1,53 +1,45 @@ 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. */ - 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); - } - - const lsVersion = (await this.lsApi.getMetadata()).version; - const sha256 = await this.lsApi.getSha256Checksum(lsPlatform); - const checksum = await this.downloadLs(lsPath, lsPlatform, sha256); + private async getCliExecutable(platform: CliSupportedPlatform): Promise { + const cliPath = await CliExecutable.getPath(this.extensionContext.extensionPath, await this.configuration.getCliPath()); + const lsVersion = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const sha256 = await this.cliApi.getSha256Checksum(lsVersion, platform); + const checksum = await this.downloadCli(cliPath, platform, sha256); if (!checksum) { return null; @@ -58,16 +50,7 @@ export class Downloader { return Promise.reject(messages.integrityCheckFailed); } - return new LsExecutable(lsVersion, checksum); - } - - private async binaryExists(filePath: string): Promise { - try { - await fsPromises.access(filePath); - return true; - } catch { - return false; - } + return new CliExecutable(lsVersion, checksum); } private async deleteFileAtPath(filePath: string): Promise { @@ -78,25 +61,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 a581610a9..6faaa1d2c 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -20,10 +20,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 +51,7 @@ export class LanguageServer implements ILanguageServer { private authenticationService: IAuthenticationService, private readonly logger: ILog, private downloadService: DownloadService, + private extensionContext: ExtensionContext ) { this.downloadService = downloadService; } @@ -77,7 +79,7 @@ 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 +94,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 +112,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,7 +147,7 @@ export class LanguageServer implements ILanguageServer { }); }); - client.onNotification(SNYK_CLI_PATH, ({ cliPath }: { cliPath: string }) => { + client.onNotification(SNYK_CLI_PATH, async ({ cliPath }: { cliPath: string }) => { if (!cliPath) { ErrorHandler.handle( new Error("CLI path wasn't provided by language server on $/snyk.isAvailableCli notification " + cliPath), @@ -155,7 +157,7 @@ export class LanguageServer implements ILanguageServer { return; } - const currentCliPath = this.configuration.getCliPath(); + const currentCliPath = await this.configuration.getCliPath(); if (currentCliPath != cliPath) { this.logger.info('Setting Snyk CLI path to: ' + cliPath); void this.configuration @@ -184,7 +186,7 @@ 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..9cc129c79 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,7 @@ 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 f4863c147..e6edf404d 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 @@ -47,7 +49,7 @@ 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; @@ -67,7 +69,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 40600c0a0..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://static.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/services/downloadService.ts b/src/snyk/common/services/downloadService.ts index 8cf86d0cc..d8c489106 100644 --- a/src/snyk/common/services/downloadService.ts +++ b/src/snyk/common/services/downloadService.ts @@ -2,39 +2,32 @@ 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 } 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'; 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 lsApi: 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, lsApi, window, logger, this.extensionContext); } async downloadOrUpdate(): Promise { - const lsInstalled = await this.isLsInstalled(); + const lsInstalled = await this.isCliInstalled(); if (!this.configuration.isAutomaticDependencyManagementEnabled()) { this.downloadReady$.next(); return false; @@ -59,20 +52,21 @@ 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; + const platform = await CliExecutable.getCurrentWithArch(); + const version = await this.lsApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const lsInstalled = await this.isCliInstalled(); + const cliVersionHasUpdated = this.hasCliVersionUpdated(version); + const needsUpdate = cliVersionHasUpdated; if (!lsInstalled || needsUpdate) { - const updateAvailable = await this.isLsUpdateAvailable(platform); + const updateAvailable = await this.isCliUpdateAvailable(platform); if (!updateAvailable) { return false; } @@ -81,25 +75,31 @@ 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; } return false; } - async isLsInstalled() { - const lsExecutableExists = await LsExecutable.exists(this.configuration); - const lastUpdateDateWritten = !!this.getLastLsUpdateDate(); - const lsChecksumWritten = !!this.getLsChecksum(); + async isCliInstalled() { + const lsExecutableExists = await CliExecutable.exists( + this.extensionContext.extensionPath, + await this.configuration.getCliPath(), + ); + const lsChecksumWritten = !!this.getCliChecksum(); - return lsExecutableExists && lastUpdateDateWritten && lsChecksumWritten; + return lsExecutableExists && lsChecksumWritten; } - 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.lsApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const latestChecksum = await this.lsApi.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,37 +111,24 @@ 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 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 setCliChecksum(checksum: Checksum): Promise { + await this.extensionContext.updateGlobalStateValue(MEMENTO_CLI_CHECKSUM, checksum.checksum); } - private hasLspVersionUpdated(): boolean { - const currentProtoclVersion = this.getLsProtocolVersion(); - return currentProtoclVersion != PROTOCOL_VERSION; + private async setCliVersion(cliVersion: string): Promise { + await this.extensionContext.updateGlobalStateValue(MEMENTO_CLI_VERSION, cliVersion); } - private getLastLsUpdateDate() { - return this.extensionContext.getGlobalStateValue(MEMENTO_LS_LAST_UPDATE_DATE); + private hasCliVersionUpdated(cliVersion: string): boolean { + const currentVersion = this.getCliVersion(); + return currentVersion != cliVersion; } - private getLsProtocolVersion() { - return this.extensionContext.getGlobalStateValue(MEMENTO_LS_PROTOCOL_VERSION); + private getCliVersion(): string | undefined { + return this.extensionContext.getGlobalStateValue(MEMENTO_CLI_VERSION); } - private getLsChecksum(): number | undefined { - return this.extensionContext.getGlobalStateValue(MEMENTO_LS_CHECKSUM); + 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..9170f0d5c 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 7404aec7a..7c55c3b14 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -41,7 +41,7 @@ import { ErrorHandler } from './common/error/errorHandler'; import { ErrorReporter } from './common/error/errorReporter'; import { ExperimentService } from './common/experiment/services/experimentService'; import { LanguageServer } from './common/languageServer/languageServer'; -import { StaticLsApi } from './common/languageServer/staticLsApi'; +import { StaticCliApi } from './cli/staticCliApi'; import { Logger } from './common/logger/logger'; import { DownloadService } from './common/services/downloadService'; import { LearnService } from './common/services/learnService'; @@ -50,7 +50,7 @@ import { User } from './common/user'; import { CodeActionAdapter } from './common/vscode/codeAction'; import { vsCodeCommands } from './common/vscode/commands'; import { vsCodeEnv } from './common/vscode/env'; -import { extensionContext } from './common/vscode/extensionContext'; +import { ExtensionContext, extensionContext } from './common/vscode/extensionContext'; import { LanguageClientAdapter } from './common/vscode/languageClient'; import { vsCodeLanguages } from './common/vscode/languages'; import SecretStorageAdapter from './common/vscode/secretStorage'; @@ -87,7 +87,6 @@ class SnykExtension extends SnykLib implements IExtension { public async activate(vscodeContext: vscode.ExtensionContext): Promise { extensionContext.setContext(vscodeContext); this.context = extensionContext; - const snykConfiguration = await this.getSnykConfiguration(); if (snykConfiguration) { await ErrorReporter.init(configuration, snykConfiguration, extensionContext.extensionPath, vsCodeEnv, Logger); @@ -101,6 +100,10 @@ class SnykExtension extends SnykLib implements IExtension { } } + public static getExtensionContext(): ExtensionContext { + return extensionContext; + } + private configureGitHandlers(): void { // Get the Git extension const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports; @@ -185,7 +188,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, ); @@ -201,6 +204,7 @@ class SnykExtension extends SnykLib implements IExtension { this.authService, Logger, this.downloadService, + this.context, ); const codeSuggestionProvider = new CodeSuggestionWebviewProvider( @@ -432,7 +436,6 @@ class SnykExtension extends SnykLib implements IExtension { // initialize contexts await this.contextService.setContext(SNYK_CONTEXT.INITIALIZED, true); - // Actually start analysis this.runScan(); } @@ -443,12 +446,16 @@ class SnykExtension extends SnykLib implements IExtension { await ErrorReporter.flush(); } + 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/common/languageServer/lsExecutable.test.ts b/src/test/unit/common/languageServer/lsExecutable.test.ts index 186bdfc53..1435a8607 100644 --- a/src/test/unit/common/languageServer/lsExecutable.test.ts +++ b/src/test/unit/common/languageServer/lsExecutable.test.ts @@ -2,8 +2,8 @@ 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'; +import { CliExecutable } from '../../../../snyk/cli/cliExecutable'; +import { LsSupportedPlatform } from '../../../../snyk/cli/supportedPlatforms'; suite('LsExecutable', () => { teardown(() => { @@ -11,29 +11,18 @@ suite('LsExecutable', () => { }); 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`); + strictEqual(CliExecutable.getFileName('darwinAmd64'), 'snyk-ls_darwin_amd64'); + strictEqual(CliExecutable.getFileName('darwinArm64'), 'snyk-ls_darwin_arm64'); + strictEqual(CliExecutable.getFileName('linux386'), 'snyk-ls_linux_386'); + strictEqual(CliExecutable.getFileName('linuxAmd64'), 'snyk-ls_linux_amd64'); + strictEqual(CliExecutable.getFileName('linuxArm64'), 'snyk-ls_linux_arm64'); + strictEqual(CliExecutable.getFileName('windows386'), 'snyk-ls_windows_386.exe'); + strictEqual(CliExecutable.getFileName('windowsAmd64'), 'snyk-ls_windows_amd64.exe'); }); test('Returns correct paths', () => { const homedirStub = sinon.stub(os, 'homedir'); - const getCurrentWithArchStub = sinon.stub(LsExecutable, 'getCurrentWithArch'); + const getCurrentWithArchStub = sinon.stub(CliExecutable, 'getCurrentWithArch'); // DarwinAmd64 let macOSPlatform: LsSupportedPlatform = 'darwinAmd64'; @@ -41,17 +30,17 @@ suite('LsExecutable', () => { getCurrentWithArchStub.returns(macOSPlatform); homedirStub.returns(homedir); - let expectedFilename = LsExecutable.getFilename(macOSPlatform); + let expectedFilename = CliExecutable.getFileName(macOSPlatform); let expectedCliPath = path.join(homedir, '/Library/Application Support/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); + strictEqual(CliExecutable.getPath(), expectedCliPath); // DarwinArm64 macOSPlatform = 'darwinArm64'; getCurrentWithArchStub.returns(macOSPlatform); - expectedFilename = LsExecutable.getFilename(macOSPlatform); + expectedFilename = CliExecutable.getFileName(macOSPlatform); expectedCliPath = path.join(homedir, '/Library/Application Support/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); + strictEqual(CliExecutable.getPath(), expectedCliPath); // Linux386 let linuxPlatform: LsSupportedPlatform = 'linux386'; @@ -59,25 +48,25 @@ suite('LsExecutable', () => { getCurrentWithArchStub.returns(linuxPlatform); homedirStub.returns(homedir); - expectedFilename = LsExecutable.getFilename(linuxPlatform); + expectedFilename = CliExecutable.getFileName(linuxPlatform); expectedCliPath = path.join(homedir, '/.local/share/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); + strictEqual(CliExecutable.getPath(), expectedCliPath); // LinuxAmd64 linuxPlatform = 'linuxAmd64'; getCurrentWithArchStub.returns(linuxPlatform); - expectedFilename = LsExecutable.getFilename(linuxPlatform); + expectedFilename = CliExecutable.getFileName(linuxPlatform); expectedCliPath = path.join(homedir, '/.local/share/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); + strictEqual(CliExecutable.getPath(), expectedCliPath); // LinuxArm64 linuxPlatform = 'linuxArm64'; getCurrentWithArchStub.returns(linuxPlatform); - expectedFilename = LsExecutable.getFilename(linuxPlatform); + expectedFilename = CliExecutable.getFileName(linuxPlatform); expectedCliPath = path.join(homedir, '/.local/share/', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); + strictEqual(CliExecutable.getPath(), expectedCliPath); // Windows386 let windowsPlatform: LsSupportedPlatform = 'windows386'; @@ -85,22 +74,22 @@ suite('LsExecutable', () => { getCurrentWithArchStub.returns(windowsPlatform); homedirStub.returns(homedir); - expectedFilename = LsExecutable.getFilename(windowsPlatform); + expectedFilename = CliExecutable.getFileName(windowsPlatform); expectedCliPath = path.join(homedir, '\\AppData\\Local\\', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); + strictEqual(CliExecutable.getPath(), expectedCliPath); // WindowsAmd64 windowsPlatform = 'windowsAmd64'; getCurrentWithArchStub.returns(windowsPlatform); - expectedFilename = LsExecutable.getFilename(windowsPlatform); + expectedFilename = CliExecutable.getFileName(windowsPlatform); expectedCliPath = path.join(homedir, '\\AppData\\Local\\', 'snyk-ls', expectedFilename); - strictEqual(LsExecutable.getPath(), expectedCliPath); + strictEqual(CliExecutable.getPath(), expectedCliPath); }); test('Return custom path, if provided', () => { const customPath = '/path/to/cli'; - strictEqual(LsExecutable.getPath(customPath), customPath); + strictEqual(CliExecutable.getPath(customPath), customPath); }); test('Returns correct platform architecture', () => { @@ -110,28 +99,28 @@ suite('LsExecutable', () => { // OSX platformStub.returns('darwin'); archStub.returns('x64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'darwinAmd64'); + strictEqual(CliExecutable.getCurrentWithArch(), 'darwinAmd64'); archStub.returns('arm64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'darwinArm64'); + strictEqual(CliExecutable.getCurrentWithArch(), 'darwinArm64'); // Linux platformStub.returns('linux'); archStub.returns('x64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'linuxAmd64'); + strictEqual(CliExecutable.getCurrentWithArch(), 'linuxAmd64'); archStub.returns('arm64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'linuxArm64'); + strictEqual(CliExecutable.getCurrentWithArch(), 'linuxArm64'); archStub.returns('ia32'); - strictEqual(LsExecutable.getCurrentWithArch(), 'linux386'); + strictEqual(CliExecutable.getCurrentWithArch(), 'linux386'); // Windows platformStub.returns('win32'); archStub.returns('x64'); - strictEqual(LsExecutable.getCurrentWithArch(), 'windowsAmd64'); + strictEqual(CliExecutable.getCurrentWithArch(), 'windowsAmd64'); archStub.returns('ia32'); - strictEqual(LsExecutable.getCurrentWithArch(), 'windows386'); + strictEqual(CliExecutable.getCurrentWithArch(), 'windows386'); }); }); diff --git a/src/test/unit/common/services/downloadService.test.ts b/src/test/unit/common/services/downloadService.test.ts index 864867989..bc15d393d 100644 --- a/src/test/unit/common/services/downloadService.test.ts +++ b/src/test/unit/common/services/downloadService.test.ts @@ -4,14 +4,14 @@ import { Checksum } from '../../../../snyk/cli/checksum'; import { CliExecutable } from '../../../../snyk/cli/cliExecutable'; import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; import { - MEMENTO_LS_CHECKSUM, + MEMENTO_CLI_CHECKSUM, MEMENTO_LS_LAST_UPDATE_DATE, - MEMENTO_LS_PROTOCOL_VERSION, + MEMENTO_CLI_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 +21,7 @@ import { windowMock } from '../../mocks/window.mock'; suite('DownloadService', () => { let logger: ILog; - let lsApi: IStaticLsApi; + let lsApi: IStaticCliApi; let context: ExtensionContext; let downloader: Downloader; let configuration: IConfiguration; @@ -138,7 +138,7 @@ suite('DownloadService', () => { 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))); + sinon.stub(downloader, 'download').resolves(new CliExecutable('1.0.0', new Checksum(latestChecksumStr))); const updated = await service.update(); @@ -151,9 +151,9 @@ suite('DownloadService', () => { 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); + contextGetGlobalStateValue.withArgs(MEMENTO_CLI_VERSION).returns(PROTOCOL_VERSION); - sinon.stub(downloader, 'download').resolves(new LsExecutable('1.0.0', new Checksum('test'))); + sinon.stub(downloader, 'download').resolves(new CliExecutable('1.0.0', new Checksum('test'))); const updated = await service.update(); @@ -166,7 +166,7 @@ suite('DownloadService', () => { 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); + contextGetGlobalStateValue.withArgs(MEMENTO_CLI_VERSION).returns(PROTOCOL_VERSION - 1); stubSuccessDownload(apigetSha256Checksum, downloader); @@ -182,7 +182,7 @@ suite('DownloadService', () => { 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_CLI_CHECKSUM).returns(undefined); contextGetGlobalStateValue.withArgs(MEMENTO_LS_LAST_UPDATE_DATE).returns(undefined); stub(CliExecutable, 'exists').resolves(true); @@ -223,5 +223,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..0fae04273 100644 --- a/src/test/unit/download/downloader.test.ts +++ b/src/test/unit/download/downloader.test.ts @@ -4,22 +4,22 @@ 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, CliMetadata } from '../../../snyk/cli/staticCliApi'; import { ILog } from '../../../snyk/common/logger/interfaces'; import { LoggerMock } from '../mocks/logger.mock'; import { windowMock } from '../mocks/window.mock'; suite('LS Downloader (LS)', () => { let logger: ILog; - let lsApi: IStaticLsApi; + let lsApi: IStaticCliApi; let configuration: IConfiguration; setup(() => { lsApi = { getDownloadUrl: sinon.fake(), downloadBinary: sinon.fake(), - getMetadata(): Promise { + getMetadata(): Promise { return Promise.resolve({ commit: 'abc', date: '01.01.2001', @@ -50,26 +50,26 @@ suite('LS Downloader (LS)', () => { 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()); + 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); - sinon.stub(LsExecutable, 'getCurrentWithArch').returns('darwinArm64'); + sinon.stub(CliExecutable, 'getCurrentWithArch').returns('darwinArm64'); sinon.stub(fs, 'access').returns(Promise.resolve()); const unlink = sinon.stub(fs, 'unlink'); await downloader.download(); - const lsPath = LsExecutable.getPath(configuration.getSnykLanguageServerPath()); + const lsPath = CliExecutable.getPath(configuration.getSnykLanguageServerPath()); strictEqual(unlink.calledOnceWith(lsPath), true); }); test('Rejects downloaded LS when integrity check fails', async () => { const downloader = new Downloader(configuration, lsApi, windowMock, logger); - sinon.stub(LsExecutable, 'getCurrentWithArch').returns('darwinAmd64'); + sinon.stub(CliExecutable, 'getCurrentWithArch').returns('darwinAmd64'); sinon.stub(fs); sinon.stub(downloader, 'downloadLs').resolves(new Checksum('test')); From 2fb862479ee8d05908731cb691e3a54344521f04 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Mon, 28 Oct 2024 17:56:50 +0100 Subject: [PATCH 02/23] chore: remove comment --- src/snyk/common/services/downloadService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/snyk/common/services/downloadService.ts b/src/snyk/common/services/downloadService.ts index d8c489106..d4f16af21 100644 --- a/src/snyk/common/services/downloadService.ts +++ b/src/snyk/common/services/downloadService.ts @@ -59,7 +59,6 @@ export class DownloadService { } async update(): Promise { - // let language server manage CLI downloads, but download LS here const platform = await CliExecutable.getCurrentWithArch(); const version = await this.lsApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); const lsInstalled = await this.isCliInstalled(); From 87e247af499796a0966a0561f0531029a56ea9f3 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Tue, 29 Oct 2024 12:05:05 +0100 Subject: [PATCH 03/23] chore: lint --- src/snyk/cli/cliExecutable.ts | 4 ++-- src/snyk/cli/staticCliApi.ts | 4 ++-- src/snyk/common/configuration/configuration.ts | 5 +---- src/snyk/common/download/downloader.ts | 7 +++++-- src/snyk/common/languageServer/languageServer.ts | 15 +++++++++++---- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/snyk/cli/cliExecutable.ts b/src/snyk/cli/cliExecutable.ts index 6d3e3234e..e6ff7f5bb 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -30,8 +30,8 @@ export class CliExecutable { static async getCurrentWithArch(): Promise { let platform = ''; - let osName = os.platform().toString().toLowerCase(); - let archName = os.arch().toLowerCase(); + const osName = os.platform().toString().toLowerCase(); + const archName = os.arch().toLowerCase(); if (osName === 'linux') { if (await this.isAlpine()) { platform = 'linux_alpine'; diff --git a/src/snyk/cli/staticCliApi.ts b/src/snyk/cli/staticCliApi.ts index 6487a684a..1fcb26847 100644 --- a/src/snyk/cli/staticCliApi.ts +++ b/src/snyk/cli/staticCliApi.ts @@ -27,7 +27,7 @@ export class StaticCliApi implements IStaticCliApi { } getDownloadUrl(version: string, platform: CliSupportedPlatform): string { - if(!version.startsWith('v')) { + if (!version.startsWith('v')) { version = `v${version}`; } const downloadUrl = `${this.configuration.getCliBaseDownloadUrl()}/cli/${version}/${this.getFileName(platform)}`; @@ -69,7 +69,7 @@ export class StaticCliApi implements IStaticCliApi { } async getSha256Checksum(version: string, platform: CliSupportedPlatform): Promise { - const fileName = await this.getFileName(platform); + const fileName = this.getFileName(platform); const { data } = await axios.get( `${this.getSha256DownloadUrl(version, platform)}`, await getAxiosConfig(this.workspace, this.configuration, this.logger), diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 743a272f0..41bf17e15 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -156,10 +156,7 @@ export class Configuration implements IConfiguration { private featureFlag: { [key: string]: boolean } = {}; - constructor( - private processEnv: NodeJS.ProcessEnv = process.env, - private workspace: IVSCodeWorkspace - ) {} + constructor(private processEnv: NodeJS.ProcessEnv = process.env, private workspace: IVSCodeWorkspace) {} async setCliReleaseChannel(releaseChannel: string): Promise { if (!releaseChannel) return; return this.workspace.updateConfiguration( diff --git a/src/snyk/common/download/downloader.ts b/src/snyk/common/download/downloader.ts index 97d060a63..4a130818e 100644 --- a/src/snyk/common/download/downloader.ts +++ b/src/snyk/common/download/downloader.ts @@ -22,7 +22,7 @@ export class Downloader { private readonly cliApi: IStaticCliApi, private readonly window: IVSCodeWindow, private readonly logger: ILog, - private readonly extensionContext: ExtensionContext + private readonly extensionContext: ExtensionContext, ) {} /** * Downloads LS. Existing executable is deleted. @@ -36,7 +36,10 @@ export class Downloader { } private async getCliExecutable(platform: CliSupportedPlatform): Promise { - const cliPath = await CliExecutable.getPath(this.extensionContext.extensionPath, await this.configuration.getCliPath()); + const cliPath = await CliExecutable.getPath( + this.extensionContext.extensionPath, + await this.configuration.getCliPath(), + ); const lsVersion = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); const sha256 = await this.cliApi.getSha256Checksum(lsVersion, platform); const checksum = await this.downloadCli(cliPath, platform, sha256); diff --git a/src/snyk/common/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index c3ec9fb3a..5b6fc48fd 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -51,7 +51,7 @@ export class LanguageServer implements ILanguageServer { private authenticationService: IAuthenticationService, private readonly logger: ILog, private downloadService: DownloadService, - private extensionContext: ExtensionContext + private extensionContext: ExtensionContext, ) { this.downloadService = downloadService; } @@ -79,7 +79,10 @@ export class LanguageServer implements ILanguageServer { }; } - const cliBinaryPath = await CliExecutable.getPath(this.extensionContext.extensionPath, await this.configuration.getCliPath()); + const cliBinaryPath = await CliExecutable.getPath( + this.extensionContext.extensionPath, + await this.configuration.getCliPath(), + ); // log level is set to info by default let logLevel = 'info'; @@ -147,7 +150,7 @@ export class LanguageServer implements ILanguageServer { }); }); - client.onNotification(SNYK_CLI_PATH, async ({ cliPath }: { cliPath: string }) => { + client.onNotification(SNYK_CLI_PATH, async ({ cliPath }: { cliPath: string }) => { if (!cliPath) { ErrorHandler.handle( new Error("CLI path wasn't provided by language server on $/snyk.isAvailableCli notification " + cliPath), @@ -186,7 +189,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, this.extensionContext); + const settings = await LanguageServerSettings.fromConfiguration( + this.configuration, + this.user, + this.extensionContext, + ); return settings; } From fabebc70057ac7cb064f04ff56ddccac0c7964db Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Tue, 29 Oct 2024 12:34:48 +0100 Subject: [PATCH 04/23] fix: wip tests --- src/test/unit/cli/cliExecutable.test.ts | 8 +- .../languageServer/languageServer.test.ts | 26 +++- .../languageServer/lsExecutable.test.ts | 126 ------------------ .../common/services/downloadService.test.ts | 114 +--------------- src/test/unit/download/downloader.test.ts | 15 +-- 5 files changed, 33 insertions(+), 256 deletions(-) delete mode 100644 src/test/unit/common/languageServer/lsExecutable.test.ts diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index ceb8d6f27..6ebb383b5 100644 --- a/src/test/unit/cli/cliExecutable.test.ts +++ b/src/test/unit/cli/cliExecutable.test.ts @@ -10,9 +10,11 @@ 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', () => { diff --git a/src/test/unit/common/languageServer/languageServer.test.ts b/src/test/unit/common/languageServer/languageServer.test.ts index 5579f58f5..98d7af307 100644 --- a/src/test/unit/common/languageServer/languageServer.test.ts +++ b/src/test/unit/common/languageServer/languageServer.test.ts @@ -10,7 +10,12 @@ import { ServerSettings } from '../../../../snyk/common/languageServer/settings' import { DownloadService } from '../../../../snyk/common/services/downloadService'; import { User } from '../../../../snyk/common/user'; import { ILanguageClientAdapter } from '../../../../snyk/common/vscode/languageClient'; -import { LanguageClient, LanguageClientOptions, ServerOptions } from '../../../../snyk/common/vscode/types'; +import { + ExtensionContext, + LanguageClient, + LanguageClientOptions, + ServerOptions, +} from '../../../../snyk/common/vscode/types'; import { IVSCodeWorkspace } from '../../../../snyk/common/vscode/workspace'; import { defaultFeaturesConfigurationStub } from '../../mocks/configuration.mock'; import { LoggerMock } from '../../mocks/logger.mock'; @@ -25,6 +30,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 +41,8 @@ suite('Language Server', () => { }, } as unknown as LoggerMock; + let contextGetGlobalStateValue: sinon.SinonStub; + setup(() => { configurationMock = { getAuthenticationMethod(): string { @@ -46,8 +54,8 @@ suite('Language Server', () => { getDeltaFindingsEnabled(): boolean { return false; }, - getCliPath(): string | undefined { - return path; + getCliPath(): Promise { + return Promise.resolve(path); }, getToken(): Promise { return Promise.resolve('testToken'); @@ -86,6 +94,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 +148,7 @@ suite('Language Server', () => { authServiceMock, logger, downloadServiceMock, + extensionContextMock ); downloadServiceMock.downloadReady$.next(); @@ -179,6 +198,7 @@ suite('Language Server', () => { authServiceMock, new LoggerMock(), downloadServiceMock, + extensionContextMock ); downloadServiceMock.downloadReady$.next(); await languageServer.start(); 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 1435a8607..000000000 --- a/src/test/unit/common/languageServer/lsExecutable.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { strictEqual } from 'assert'; -import os from 'os'; -import path from 'path'; -import sinon from 'sinon'; -import { CliExecutable } from '../../../../snyk/cli/cliExecutable'; -import { LsSupportedPlatform } from '../../../../snyk/cli/supportedPlatforms'; - -suite('LsExecutable', () => { - teardown(() => { - sinon.restore(); - }); - - test('Returns correct filename for different platforms', () => { - strictEqual(CliExecutable.getFileName('darwinAmd64'), 'snyk-ls_darwin_amd64'); - strictEqual(CliExecutable.getFileName('darwinArm64'), 'snyk-ls_darwin_arm64'); - strictEqual(CliExecutable.getFileName('linux386'), 'snyk-ls_linux_386'); - strictEqual(CliExecutable.getFileName('linuxAmd64'), 'snyk-ls_linux_amd64'); - strictEqual(CliExecutable.getFileName('linuxArm64'), 'snyk-ls_linux_arm64'); - strictEqual(CliExecutable.getFileName('windows386'), 'snyk-ls_windows_386.exe'); - strictEqual(CliExecutable.getFileName('windowsAmd64'), 'snyk-ls_windows_amd64.exe'); - }); - - test('Returns correct paths', () => { - const homedirStub = sinon.stub(os, 'homedir'); - const getCurrentWithArchStub = sinon.stub(CliExecutable, 'getCurrentWithArch'); - - // DarwinAmd64 - let macOSPlatform: LsSupportedPlatform = 'darwinAmd64'; - let homedir = '/Users/user'; - getCurrentWithArchStub.returns(macOSPlatform); - homedirStub.returns(homedir); - - let expectedFilename = CliExecutable.getFileName(macOSPlatform); - let expectedCliPath = path.join(homedir, '/Library/Application Support/', 'snyk-ls', expectedFilename); - strictEqual(CliExecutable.getPath(), expectedCliPath); - - // DarwinArm64 - macOSPlatform = 'darwinArm64'; - getCurrentWithArchStub.returns(macOSPlatform); - - expectedFilename = CliExecutable.getFileName(macOSPlatform); - expectedCliPath = path.join(homedir, '/Library/Application Support/', 'snyk-ls', expectedFilename); - strictEqual(CliExecutable.getPath(), expectedCliPath); - - // Linux386 - let linuxPlatform: LsSupportedPlatform = 'linux386'; - homedir = '/home/user'; - getCurrentWithArchStub.returns(linuxPlatform); - homedirStub.returns(homedir); - - expectedFilename = CliExecutable.getFileName(linuxPlatform); - expectedCliPath = path.join(homedir, '/.local/share/', 'snyk-ls', expectedFilename); - strictEqual(CliExecutable.getPath(), expectedCliPath); - - // LinuxAmd64 - linuxPlatform = 'linuxAmd64'; - getCurrentWithArchStub.returns(linuxPlatform); - - expectedFilename = CliExecutable.getFileName(linuxPlatform); - expectedCliPath = path.join(homedir, '/.local/share/', 'snyk-ls', expectedFilename); - strictEqual(CliExecutable.getPath(), expectedCliPath); - - // LinuxArm64 - linuxPlatform = 'linuxArm64'; - getCurrentWithArchStub.returns(linuxPlatform); - - expectedFilename = CliExecutable.getFileName(linuxPlatform); - expectedCliPath = path.join(homedir, '/.local/share/', 'snyk-ls', expectedFilename); - strictEqual(CliExecutable.getPath(), expectedCliPath); - - // Windows386 - let windowsPlatform: LsSupportedPlatform = 'windows386'; - homedir = 'C:\\Users\\user'; - getCurrentWithArchStub.returns(windowsPlatform); - homedirStub.returns(homedir); - - expectedFilename = CliExecutable.getFileName(windowsPlatform); - expectedCliPath = path.join(homedir, '\\AppData\\Local\\', 'snyk-ls', expectedFilename); - strictEqual(CliExecutable.getPath(), expectedCliPath); - - // WindowsAmd64 - windowsPlatform = 'windowsAmd64'; - getCurrentWithArchStub.returns(windowsPlatform); - - expectedFilename = CliExecutable.getFileName(windowsPlatform); - expectedCliPath = path.join(homedir, '\\AppData\\Local\\', 'snyk-ls', expectedFilename); - strictEqual(CliExecutable.getPath(), expectedCliPath); - }); - - test('Return custom path, if provided', () => { - const customPath = '/path/to/cli'; - strictEqual(CliExecutable.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(CliExecutable.getCurrentWithArch(), 'darwinAmd64'); - - archStub.returns('arm64'); - strictEqual(CliExecutable.getCurrentWithArch(), 'darwinArm64'); - - // Linux - platformStub.returns('linux'); - archStub.returns('x64'); - strictEqual(CliExecutable.getCurrentWithArch(), 'linuxAmd64'); - - archStub.returns('arm64'); - strictEqual(CliExecutable.getCurrentWithArch(), 'linuxArm64'); - - archStub.returns('ia32'); - strictEqual(CliExecutable.getCurrentWithArch(), 'linux386'); - - // Windows - platformStub.returns('win32'); - archStub.returns('x64'); - strictEqual(CliExecutable.getCurrentWithArch(), 'windowsAmd64'); - - archStub.returns('ia32'); - strictEqual(CliExecutable.getCurrentWithArch(), 'windows386'); - }); -}); diff --git a/src/test/unit/common/services/downloadService.test.ts b/src/test/unit/common/services/downloadService.test.ts index bc15d393d..49128895c 100644 --- a/src/test/unit/common/services/downloadService.test.ts +++ b/src/test/unit/common/services/downloadService.test.ts @@ -1,14 +1,7 @@ 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_CLI_CHECKSUM, - MEMENTO_LS_LAST_UPDATE_DATE, - MEMENTO_CLI_VERSION, -} from '../../../../snyk/common/constants/globalState'; -import { PROTOCOL_VERSION } from '../../../../snyk/common/constants/languageServer'; import { Downloader } from '../../../../snyk/common/download/downloader'; import { CliExecutable } from '../../../../snyk/cli/cliExecutable'; import { IStaticCliApi } from '../../../../snyk/cli/staticCliApi'; @@ -35,9 +28,8 @@ suite('DownloadService', () => { apigetSha256Checksum = sinon.stub(); lsApi = { - getDownloadUrl: sinon.fake(), + getLatestCliVersion: sinon.fake(), downloadBinary: sinon.fake(), - getMetadata: sinon.fake(), getSha256Checksum: apigetSha256Checksum, }; @@ -59,7 +51,7 @@ suite('DownloadService', () => { getSnykLanguageServerPath: () => 'ab/c', } as unknown as IConfiguration; - downloader = new Downloader(configuration, lsApi, windowMock, logger); + downloader = new Downloader(configuration, lsApi, windowMock, logger, context); }); teardown(() => { @@ -88,7 +80,7 @@ suite('DownloadService', () => { getSnykLanguageServerPath: () => 'abc/d', } as unknown as IConfiguration; const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); - stub(service, 'isLsInstalled').resolves(true); + stub(service, 'isCliInstalled').resolves(true); const downloadSpy = stub(service, 'download'); const updateSpy = stub(service, 'update'); @@ -98,104 +90,6 @@ 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 CliExecutable('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_CLI_VERSION).returns(PROTOCOL_VERSION); - - sinon.stub(downloader, 'download').resolves(new CliExecutable('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_CLI_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_CLI_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 () => { configuration = { isAutomaticDependencyManagementEnabled: () => false, @@ -203,7 +97,7 @@ suite('DownloadService', () => { getSnykLanguageServerPath: () => 'abc/d', } as unknown as IConfiguration; const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); - stub(service, 'isLsInstalled').resolves(false); + stub(service, 'isCliInstalled').resolves(false); const downloadSpy = stub(service, 'download'); const updateSpy = stub(service, 'update'); diff --git a/src/test/unit/download/downloader.test.ts b/src/test/unit/download/downloader.test.ts index 0fae04273..edd3b1324 100644 --- a/src/test/unit/download/downloader.test.ts +++ b/src/test/unit/download/downloader.test.ts @@ -5,7 +5,7 @@ import { Checksum } from '../../../snyk/cli/checksum'; import { IConfiguration } from '../../../snyk/common/configuration/configuration'; import { Downloader } from '../../../snyk/common/download/downloader'; import { CliExecutable } from '../../../snyk/cli/cliExecutable'; -import { IStaticCliApi, CliMetadata } from '../../../snyk/cli/staticCliApi'; +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'; @@ -19,19 +19,6 @@ suite('LS Downloader (LS)', () => { lsApi = { getDownloadUrl: 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(); From 104fe14a1bd9252aa2858a628fe7eccafbd8b9f7 Mon Sep 17 00:00:00 2001 From: DariusZdroba Date: Tue, 29 Oct 2024 14:49:50 +0200 Subject: [PATCH 05/23] fix: wip tests --- .../languageServer/languageServer.test.ts | 16 ++++---- .../common/languageServer/middleware.test.ts | 21 ++++++++--- .../common/languageServer/settings.test.ts | 18 ++++++++- src/test/unit/download/downloader.test.ts | 37 ++++++++++++------- 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/src/test/unit/common/languageServer/languageServer.test.ts b/src/test/unit/common/languageServer/languageServer.test.ts index 98d7af307..d35150b0f 100644 --- a/src/test/unit/common/languageServer/languageServer.test.ts +++ b/src/test/unit/common/languageServer/languageServer.test.ts @@ -10,18 +10,14 @@ import { ServerSettings } from '../../../../snyk/common/languageServer/settings' import { DownloadService } from '../../../../snyk/common/services/downloadService'; import { User } from '../../../../snyk/common/user'; import { ILanguageClientAdapter } from '../../../../snyk/common/vscode/languageClient'; -import { - ExtensionContext, - LanguageClient, - LanguageClientOptions, - ServerOptions, -} from '../../../../snyk/common/vscode/types'; +import { LanguageClient, LanguageClientOptions, ServerOptions } from '../../../../snyk/common/vscode/types'; import { IVSCodeWorkspace } from '../../../../snyk/common/vscode/workspace'; import { defaultFeaturesConfigurationStub } from '../../mocks/configuration.mock'; 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; @@ -92,7 +88,7 @@ suite('Language Server', () => { return []; }, scanningMode: 'auto', - } as IConfiguration; + } as unknown as IConfiguration; extensionContextMock = { extensionPath: 'test/path', @@ -148,7 +144,7 @@ suite('Language Server', () => { authServiceMock, logger, downloadServiceMock, - extensionContextMock + extensionContextMock, ); downloadServiceMock.downloadReady$.next(); @@ -198,7 +194,7 @@ suite('Language Server', () => { authServiceMock, new LoggerMock(), downloadServiceMock, - extensionContextMock + extensionContextMock, ); downloadServiceMock.downloadReady$.next(); await languageServer.start(); @@ -224,6 +220,7 @@ suite('Language Server', () => { authServiceMock, new LoggerMock(), downloadServiceMock, + extensionContextMock, ); }); @@ -269,6 +266,7 @@ suite('Language Server', () => { authServiceMock, new LoggerMock(), downloadServiceMock, + extensionContextMock, ); const initOptions = await languageServer.getInitializationOptions(); diff --git a/src/test/unit/common/languageServer/middleware.test.ts b/src/test/unit/common/languageServer/middleware.test.ts index f15244631..2327a1089 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; @@ -56,7 +58,16 @@ suite('Language Server: Middleware', () => { getFolderConfigs(): FolderConfig[] { return []; }, - } as IConfiguration; + } as unknown 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()), + 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/download/downloader.test.ts b/src/test/unit/download/downloader.test.ts index edd3b1324..539e41dd3 100644 --- a/src/test/unit/download/downloader.test.ts +++ b/src/test/unit/download/downloader.test.ts @@ -9,25 +9,34 @@ 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)', () => { let logger: ILog; - let lsApi: IStaticCliApi; + let cliApi: IStaticCliApi; let configuration: IConfiguration; - + let extensionContextMock: ExtensionContext; setup(() => { - lsApi = { - getDownloadUrl: sinon.fake(), + cliApi = { + getLatestCliVersion: sinon.fake(), downloadBinary: sinon.fake(), getSha256Checksum: sinon.fake(), }; logger = new LoggerMock(); configuration = { isAutomaticDependencyManagementEnabled: () => true, - getSnykLanguageServerPath(): string { + getCliPath(): string { return 'abc/d'; }, - } as IConfiguration; + } as unknown 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 @@ -36,30 +45,30 @@ suite('LS Downloader (LS)', () => { }); test('Download of LS fails if platform is not supported', async () => { - const downloader = new Downloader(configuration, lsApi, windowMock, logger); + 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); + const downloader = new Downloader(configuration, cliApi, windowMock, logger, extensionContextMock); - sinon.stub(CliExecutable, '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 = CliExecutable.getPath(configuration.getSnykLanguageServerPath()); + const cliPath = await CliExecutable.getPath((await configuration.getCliPath()) as string); - strictEqual(unlink.calledOnceWith(lsPath), true); + strictEqual(unlink.calledOnceWith(cliPath), true); }); test('Rejects downloaded LS when integrity check fails', async () => { - const downloader = new Downloader(configuration, lsApi, windowMock, logger); - sinon.stub(CliExecutable, 'getCurrentWithArch').returns('darwinAmd64'); + 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()); }); From dd2d8a4d2908ab3819264c5799393083290345ee Mon Sep 17 00:00:00 2001 From: DariusZdroba Date: Tue, 29 Oct 2024 14:56:30 +0200 Subject: [PATCH 06/23] chore: lint --- .../common/configuration/configuration.ts | 2 +- .../common/languageServer/languageServer.ts | 32 +++++++++++-------- src/snyk/common/languageServer/middleware.ts | 6 +++- src/snyk/common/languageServer/settings.ts | 6 +++- .../common/watchers/configurationWatcher.ts | 2 +- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 41bf17e15..8e2cd019f 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -534,7 +534,7 @@ export class Configuration implements IConfiguration { const extensionContext = SnykExtension.getExtensionContext(); if (!cliPath && extensionContext) { cliPath = await CliExecutable.getPath(SnykExtension.getExtensionContext().extensionPath); - this.setCliPath(cliPath); + await this.setCliPath(cliPath); } return cliPath; } diff --git a/src/snyk/common/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index 5b6fc48fd..e1f77b47c 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -150,7 +150,7 @@ export class LanguageServer implements ILanguageServer { }); }); - client.onNotification(SNYK_CLI_PATH, async ({ cliPath }: { cliPath: string }) => { + 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), @@ -160,18 +160,24 @@ export class LanguageServer implements ILanguageServer { return; } - const currentCliPath = await 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); - }); - } + this.configuration + .getCliPath() + .then(currentCliPath => { + 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); + }); + } + }) + .catch((error: Error) => { + ErrorHandler.handle(error, this.logger, error.message); + }); }); client.onNotification(SNYK_ADD_TRUSTED_FOLDERS, ({ trustedFolders }: { trustedFolders: string[] }) => { diff --git a/src/snyk/common/languageServer/middleware.ts b/src/snyk/common/languageServer/middleware.ts index 9cc129c79..8f6d4fe41 100644 --- a/src/snyk/common/languageServer/middleware.ts +++ b/src/snyk/common/languageServer/middleware.ts @@ -41,7 +41,11 @@ export class LanguageClientMiddleware implements Middleware { return []; } - const serverSettings = await LanguageServerSettings.fromConfiguration(this.configuration, this.user, this.extensionContext); + 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 e6edf404d..a41f7587d 100644 --- a/src/snyk/common/languageServer/settings.ts +++ b/src/snyk/common/languageServer/settings.ts @@ -49,7 +49,11 @@ export type ServerSettings = { }; export class LanguageServerSettings { - static async fromConfiguration(configuration: IConfiguration, user: User, extensionContext: ExtensionContext): Promise { + static async fromConfiguration( + configuration: IConfiguration, + user: User, + extensionContext: ExtensionContext, + ): Promise { const featuresConfiguration = configuration.getFeaturesConfiguration(); const ossEnabled = _.isUndefined(featuresConfiguration.ossEnabled) ? true : featuresConfiguration.ossEnabled; diff --git a/src/snyk/common/watchers/configurationWatcher.ts b/src/snyk/common/watchers/configurationWatcher.ts index 9170f0d5c..c3846daaa 100644 --- a/src/snyk/common/watchers/configurationWatcher.ts +++ b/src/snyk/common/watchers/configurationWatcher.ts @@ -55,7 +55,7 @@ class ConfigurationWatcher implements IWatcher { } 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) { + } else if (key === ADVANCED_CLI_RELEASE_CHANNEL) { await extension.stopLanguageServer(); extension.initDependencyDownload(); return; From e2d0d8b2e294cec338c9293f14416dc9abee899b Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Tue, 29 Oct 2024 15:21:53 +0100 Subject: [PATCH 07/23] fix: tests --- src/snyk/cli/cliExecutable.ts | 6 ++--- .../common/configuration/configuration.ts | 6 ++--- src/snyk/common/download/downloader.ts | 12 ++++++++++ src/snyk/common/platform.ts | 4 ++++ src/snyk/extension.ts | 4 ---- src/test/unit/cli/cliExecutable.test.ts | 16 ++++++++----- src/test/unit/common/configuration.test.ts | 1 - .../languageServer/languageServer.test.ts | 5 +--- .../common/languageServer/middleware.test.ts | 6 ++--- .../common/services/downloadService.test.ts | 24 +++++++++---------- src/test/unit/download/downloader.test.ts | 10 ++++---- src/test/unit/mocks/workspace.mock.ts | 2 -- 12 files changed, 52 insertions(+), 44 deletions(-) diff --git a/src/snyk/cli/cliExecutable.ts b/src/snyk/cli/cliExecutable.ts index e6ff7f5bb..60245f734 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -1,8 +1,8 @@ -import os from 'os'; import path from 'path'; import fs from 'fs/promises'; import { CliSupportedPlatform } from './supportedPlatforms'; import { Checksum } from './checksum'; +import { Platform } from '../common/platform'; export class CliExecutable { public static filenameSuffixes: Record = { @@ -30,8 +30,8 @@ export class CliExecutable { static async getCurrentWithArch(): Promise { let platform = ''; - const osName = os.platform().toString().toLowerCase(); - const archName = os.arch().toLowerCase(); + const osName = Platform.getCurrent().toString().toLowerCase(); + const archName = Platform.getArch().toLowerCase(); if (osName === 'linux') { if (await this.isAlpine()) { platform = 'linux_alpine'; diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 8e2cd019f..15176b287 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -32,7 +32,7 @@ import { import SecretStorageAdapter from '../vscode/secretStorage'; import { IVSCodeWorkspace } from '../vscode/workspace'; import { CliExecutable } from '../../cli/cliExecutable'; -import SnykExtension from '../../extension'; +import { extensionContext } from '../vscode/extensionContext'; const NEWISSUES = 'Net new issues'; @@ -321,7 +321,6 @@ export class Configuration implements IConfiguration { } async setCliPath(cliPath: string | undefined): Promise { - const extensionContext = SnykExtension.getExtensionContext(); if (!cliPath && extensionContext) { cliPath = await CliExecutable.getPath(extensionContext.extensionPath); } @@ -531,9 +530,8 @@ export class Configuration implements IConfiguration { CONFIGURATION_IDENTIFIER, this.getConfigName(ADVANCED_CLI_PATH), ); - const extensionContext = SnykExtension.getExtensionContext(); if (!cliPath && extensionContext) { - cliPath = await CliExecutable.getPath(SnykExtension.getExtensionContext().extensionPath); + cliPath = await CliExecutable.getPath(extensionContext.extensionPath); await this.setCliPath(cliPath); } return cliPath; diff --git a/src/snyk/common/download/downloader.ts b/src/snyk/common/download/downloader.ts index 4a130818e..a18ce9002 100644 --- a/src/snyk/common/download/downloader.ts +++ b/src/snyk/common/download/downloader.ts @@ -40,6 +40,9 @@ export class Downloader { this.extensionContext.extensionPath, await this.configuration.getCliPath(), ); + if (await this.binaryExists(cliPath)) { + await this.deleteFileAtPath(cliPath); + } const lsVersion = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); const sha256 = await this.cliApi.getSha256Checksum(lsVersion, platform); const checksum = await this.downloadCli(cliPath, platform, sha256); @@ -56,6 +59,15 @@ export class Downloader { return new CliExecutable(lsVersion, checksum); } + private async binaryExists(filePath: string): Promise { + try { + await fsPromises.access(filePath); + return true; + } catch { + return false; + } + } + private async deleteFileAtPath(filePath: string): Promise { try { await fsPromises.unlink(filePath); 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/extension.ts b/src/snyk/extension.ts index 7c55c3b14..468e9841f 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -100,10 +100,6 @@ class SnykExtension extends SnykLib implements IExtension { } } - public static getExtensionContext(): ExtensionContext { - return extensionContext; - } - private configureGitHandlers(): void { // Get the Git extension const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports; diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index 6ebb383b5..1ba7c9866 100644 --- a/src/test/unit/cli/cliExecutable.test.ts +++ b/src/test/unit/cli/cliExecutable.test.ts @@ -17,25 +17,29 @@ suite('CliExecutable', () => { 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 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); + + sinon.stub(Platform, 'getArch').returns('arm64'); + expectedCliPath = path.join(unixExtensionDir, 'snyk-macos-arm64'); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); stub.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'); 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..450e9271d 100644 --- a/src/test/unit/common/configuration.test.ts +++ b/src/test/unit/common/configuration.test.ts @@ -3,7 +3,6 @@ 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_CUSTOM_ENDPOINT, FEATURES_PREVIEW_SETTING, diff --git a/src/test/unit/common/languageServer/languageServer.test.ts b/src/test/unit/common/languageServer/languageServer.test.ts index d35150b0f..10fcf7f58 100644 --- a/src/test/unit/common/languageServer/languageServer.test.ts +++ b/src/test/unit/common/languageServer/languageServer.test.ts @@ -57,9 +57,6 @@ suite('Language Server', () => { return Promise.resolve('testToken'); }, shouldReportErrors: true, - getSnykLanguageServerPath(): string { - return path; - }, getAdditionalCliParameters() { return '--all-projects -d'; }, @@ -88,7 +85,7 @@ suite('Language Server', () => { return []; }, scanningMode: 'auto', - } as unknown as IConfiguration; + } as IConfiguration; extensionContextMock = { extensionPath: 'test/path', diff --git a/src/test/unit/common/languageServer/middleware.test.ts b/src/test/unit/common/languageServer/middleware.test.ts index 2327a1089..2dbbd054e 100644 --- a/src/test/unit/common/languageServer/middleware.test.ts +++ b/src/test/unit/common/languageServer/middleware.test.ts @@ -32,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; }, @@ -58,7 +58,7 @@ suite('Language Server: Middleware', () => { getFolderConfigs(): FolderConfig[] { return []; }, - } as unknown as IConfiguration; + } as IConfiguration; extensionContextMock = { extensionPath: 'test/path', getGlobalStateValue: contextGetGlobalStateValue, @@ -112,7 +112,7 @@ suite('Language Server: Middleware', () => { ); assert.strictEqual( serverResult.cliPath, - CliExecutable.getPath(extensionContextMock.extensionPath, await configuration.getCliPath()), + await CliExecutable.getPath(extensionContextMock.extensionPath, await configuration.getCliPath()), ); assert.strictEqual(serverResult.enableTrustedFoldersFeature, 'true'); assert.deepStrictEqual(serverResult.trustedFolders, configuration.getTrustedFolders()); diff --git a/src/test/unit/common/services/downloadService.test.ts b/src/test/unit/common/services/downloadService.test.ts index 49128895c..6774624d3 100644 --- a/src/test/unit/common/services/downloadService.test.ts +++ b/src/test/unit/common/services/downloadService.test.ts @@ -47,9 +47,9 @@ 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, context); }); @@ -61,9 +61,9 @@ suite('DownloadService', () => { test('Tries to download LS if not installed', async () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'abc/d', - } as unknown as IConfiguration; + getCliReleaseChannel: () => "stable", + getCliPath: () => Promise.resolve("path/to/cli"), + } as IConfiguration; const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); const downloadSpy = stub(service, 'download'); const updateSpy = stub(service, 'update'); @@ -76,9 +76,9 @@ suite('DownloadService', () => { test('Tries to update LS if installed', async () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'abc/d', - } as unknown as IConfiguration; + getCliReleaseChannel: () => "stable", + getCliPath: () => Promise.resolve("path/to/cli"), + } as IConfiguration; const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); stub(service, 'isCliInstalled').resolves(true); const downloadSpy = stub(service, 'download'); @@ -93,9 +93,9 @@ suite('DownloadService', () => { test("Doesn't download LS if automatic dependency management disabled", async () => { configuration = { isAutomaticDependencyManagementEnabled: () => false, - getCustomCliPath: () => undefined, - getSnykLanguageServerPath: () => 'abc/d', - } as unknown as IConfiguration; + getCliReleaseChannel: () => "stable", + getCliPath: () => Promise.resolve("path/to/cli"), + } as IConfiguration; const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); stub(service, 'isCliInstalled').resolves(false); const downloadSpy = stub(service, 'download'); diff --git a/src/test/unit/download/downloader.test.ts b/src/test/unit/download/downloader.test.ts index 539e41dd3..2453348d5 100644 --- a/src/test/unit/download/downloader.test.ts +++ b/src/test/unit/download/downloader.test.ts @@ -25,10 +25,11 @@ suite('LS Downloader (LS)', () => { logger = new LoggerMock(); configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCliPath(): string { - return 'abc/d'; + getCliReleaseChannel: () => "stable", + getCliPath(): Promise { + return Promise.resolve('abc/d'); }, - } as unknown as IConfiguration; + } as IConfiguration; extensionContextMock = { extensionPath: 'test/path', updateGlobalStateValue: sinon.fake(), @@ -58,8 +59,7 @@ suite('LS Downloader (LS)', () => { const unlink = sinon.stub(fs, 'unlink'); await downloader.download(); - const cliPath = await CliExecutable.getPath((await configuration.getCliPath()) as string); - + const cliPath = await configuration.getCliPath() ?? ""; strictEqual(unlink.calledOnceWith(cliPath), true); }); diff --git a/src/test/unit/mocks/workspace.mock.ts b/src/test/unit/mocks/workspace.mock.ts index a46d64348..2e1ee2329 100644 --- a/src/test/unit/mocks/workspace.mock.ts +++ b/src/test/unit/mocks/workspace.mock.ts @@ -1,5 +1,3 @@ -import * as os from 'os'; -import path from 'path'; import { IVSCodeWorkspace } from '../../../snyk/common/vscode/workspace'; export function stubWorkspaceConfiguration(configSetting: string, returnValue: T | undefined): IVSCodeWorkspace { From 873730b4f2cb27ad730db7aee30abbdbec3a8152 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Tue, 29 Oct 2024 15:24:27 +0100 Subject: [PATCH 08/23] chore: lint --- src/test/unit/cli/cliExecutable.test.ts | 2 +- .../unit/common/services/downloadService.test.ts | 16 ++++++++-------- src/test/unit/download/downloader.test.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index 1ba7c9866..f76288017 100644 --- a/src/test/unit/cli/cliExecutable.test.ts +++ b/src/test/unit/cli/cliExecutable.test.ts @@ -17,7 +17,7 @@ suite('CliExecutable', () => { strictEqual(CliExecutable.getFileName('windows'), 'snyk-win.exe'); }); - test('Returns correct extension paths', async() => { + test('Returns correct extension paths', async () => { const unixExtensionDir = '/Users/user/.vscode/extensions/snyk-security.snyk-vulnerability-scanner-1.1.0'; const stub = sinon.stub(Platform, 'getCurrent').returns('darwin'); diff --git a/src/test/unit/common/services/downloadService.test.ts b/src/test/unit/common/services/downloadService.test.ts index 6774624d3..7fb1a8e24 100644 --- a/src/test/unit/common/services/downloadService.test.ts +++ b/src/test/unit/common/services/downloadService.test.ts @@ -47,8 +47,8 @@ suite('DownloadService', () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCliReleaseChannel: () => "stable", - getCliPath: () => Promise.resolve("path/to/cli"), + getCliReleaseChannel: () => 'stable', + getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; downloader = new Downloader(configuration, lsApi, windowMock, logger, context); @@ -61,8 +61,8 @@ suite('DownloadService', () => { test('Tries to download LS if not installed', async () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCliReleaseChannel: () => "stable", - getCliPath: () => Promise.resolve("path/to/cli"), + getCliReleaseChannel: () => 'stable', + getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); const downloadSpy = stub(service, 'download'); @@ -76,8 +76,8 @@ suite('DownloadService', () => { test('Tries to update LS if installed', async () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCliReleaseChannel: () => "stable", - getCliPath: () => Promise.resolve("path/to/cli"), + getCliReleaseChannel: () => 'stable', + getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); stub(service, 'isCliInstalled').resolves(true); @@ -93,8 +93,8 @@ suite('DownloadService', () => { test("Doesn't download LS if automatic dependency management disabled", async () => { configuration = { isAutomaticDependencyManagementEnabled: () => false, - getCliReleaseChannel: () => "stable", - getCliPath: () => Promise.resolve("path/to/cli"), + getCliReleaseChannel: () => 'stable', + getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); stub(service, 'isCliInstalled').resolves(false); diff --git a/src/test/unit/download/downloader.test.ts b/src/test/unit/download/downloader.test.ts index 2453348d5..4c3fb1fc2 100644 --- a/src/test/unit/download/downloader.test.ts +++ b/src/test/unit/download/downloader.test.ts @@ -25,7 +25,7 @@ suite('LS Downloader (LS)', () => { logger = new LoggerMock(); configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCliReleaseChannel: () => "stable", + getCliReleaseChannel: () => 'stable', getCliPath(): Promise { return Promise.resolve('abc/d'); }, @@ -59,7 +59,7 @@ suite('LS Downloader (LS)', () => { const unlink = sinon.stub(fs, 'unlink'); await downloader.download(); - const cliPath = await configuration.getCliPath() ?? ""; + const cliPath = (await configuration.getCliPath()) ?? ''; strictEqual(unlink.calledOnceWith(cliPath), true); }); From 15ce0d160ec71551ae869fd6eae63cdef1ca05da Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Tue, 29 Oct 2024 16:12:37 +0100 Subject: [PATCH 09/23] fix: move darwin arm64 test to last --- src/test/unit/cli/cliExecutable.test.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index f76288017..417b0404d 100644 --- a/src/test/unit/cli/cliExecutable.test.ts +++ b/src/test/unit/cli/cliExecutable.test.ts @@ -20,22 +20,23 @@ suite('CliExecutable', () => { test('Returns correct extension paths', async () => { const unixExtensionDir = '/Users/user/.vscode/extensions/snyk-security.snyk-vulnerability-scanner-1.1.0'; - const stub = sinon.stub(Platform, 'getCurrent').returns('darwin'); + const osStub = sinon.stub(Platform, 'getCurrent').returns('darwin'); let expectedCliPath = path.join(unixExtensionDir, 'snyk-macos'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); - sinon.stub(Platform, 'getArch').returns('arm64'); - expectedCliPath = path.join(unixExtensionDir, 'snyk-macos-arm64'); - strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); - - stub.returns('linux'); + osStub.returns('linux'); expectedCliPath = path.join(unixExtensionDir, 'snyk-linux'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); const winExtensionDir = `C:\\Users\\user\\.vscode\\extensions`; - stub.returns('win32'); + osStub.returns('win32'); expectedCliPath = path.join(winExtensionDir, 'snyk-win.exe'); strictEqual(await CliExecutable.getPath(winExtensionDir), expectedCliPath); + + osStub.returns('darwin'); + sinon.stub(Platform, 'getArch').returns('arm64'); + expectedCliPath = path.join(unixExtensionDir, 'snyk-macos-arm64'); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); }); test('Return custom path, if provided', async () => { From ed1adfd11ab3c91ae038561d335a6f942846ba4b Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Tue, 29 Oct 2024 16:22:24 +0100 Subject: [PATCH 10/23] fix: stub arch to x64 by default --- src/test/unit/cli/cliExecutable.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index 417b0404d..cf07989ad 100644 --- a/src/test/unit/cli/cliExecutable.test.ts +++ b/src/test/unit/cli/cliExecutable.test.ts @@ -21,6 +21,7 @@ suite('CliExecutable', () => { const unixExtensionDir = '/Users/user/.vscode/extensions/snyk-security.snyk-vulnerability-scanner-1.1.0'; const osStub = sinon.stub(Platform, 'getCurrent').returns('darwin'); + const archStub = sinon.stub(Platform, 'getArch').returns('x64'); let expectedCliPath = path.join(unixExtensionDir, 'snyk-macos'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); @@ -34,7 +35,7 @@ suite('CliExecutable', () => { strictEqual(await CliExecutable.getPath(winExtensionDir), expectedCliPath); osStub.returns('darwin'); - sinon.stub(Platform, 'getArch').returns('arm64'); + archStub.returns('arm64') expectedCliPath = path.join(unixExtensionDir, 'snyk-macos-arm64'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); }); From 43dcb329d908c8ef522de5a10517559f8f7824c6 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Tue, 29 Oct 2024 16:24:23 +0100 Subject: [PATCH 11/23] chore: update changelog --- CHANGELOG.md | 3 +++ src/test/unit/cli/cliExecutable.test.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70fa3e70a..5f5c3ffb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [2.20.0] - 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. ## [2.19.1] - Adjust OSS panel font size. diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index cf07989ad..52534adfd 100644 --- a/src/test/unit/cli/cliExecutable.test.ts +++ b/src/test/unit/cli/cliExecutable.test.ts @@ -35,7 +35,7 @@ suite('CliExecutable', () => { strictEqual(await CliExecutable.getPath(winExtensionDir), expectedCliPath); osStub.returns('darwin'); - archStub.returns('arm64') + archStub.returns('arm64'); expectedCliPath = path.join(unixExtensionDir, 'snyk-macos-arm64'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); }); From c82b0f144133dd2ec9b50e8b5b80165312aab90a Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Tue, 29 Oct 2024 16:38:52 +0100 Subject: [PATCH 12/23] fix: delete SNYK_CLI_PATH notification handling --- .../common/languageServer/languageServer.ts | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/snyk/common/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index e1f77b47c..9d70afade 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -150,36 +150,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; - } - - this.configuration - .getCliPath() - .then(currentCliPath => { - 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); - }); - } - }) - .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); From 806e367876d82914ffeaeacc16a9db7c8876de28 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Wed, 30 Oct 2024 12:17:51 +0100 Subject: [PATCH 13/23] fix: cover other arch types for cli download --- src/snyk/cli/cliExecutable.ts | 29 ++++++++++++------- src/snyk/cli/supportedPlatforms.ts | 11 ++++++- .../common/configuration/configuration.ts | 4 +-- src/snyk/common/constants/languageServer.ts | 1 - .../common/languageServer/languageServer.ts | 1 - src/test/unit/cli/cliExecutable.test.ts | 26 +++++++++++++++-- 6 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/snyk/cli/cliExecutable.ts b/src/snyk/cli/cliExecutable.ts index 60245f734..0b4d34242 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -7,14 +7,18 @@ import { Platform } from '../common/platform'; export class CliExecutable { public static filenameSuffixes: Record = { linux: 'snyk-linux', + 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 async getPath(extensionDir: string, customPath?: string): Promise { + // check if custom path is file if (customPath) { return customPath; } @@ -29,9 +33,19 @@ export class CliExecutable { } static async getCurrentWithArch(): Promise { - let platform = ''; const osName = Platform.getCurrent().toString().toLowerCase(); - const archName = Platform.getArch().toLowerCase(); + let archSuffix = Platform.getArch().toLowerCase(); + if (archSuffix !== 'arm64') { + archSuffix = ''; + } + + const platform = await this.getPlatformName(osName); + + return (platform + archSuffix) as CliSupportedPlatform; + } + + static async getPlatformName(osName: string): Promise { + let platform = ''; if (osName === 'linux') { if (await this.isAlpine()) { platform = 'linux_alpine'; @@ -39,19 +53,14 @@ export class CliExecutable { platform = 'linux'; } } else if (osName === 'darwin') { - if (archName === 'arm64') { - platform = 'macos_arm64'; - } else { - platform = 'macos'; - } - } else if (osName.includes('win')) { + platform = 'macos'; + } else if (osName === 'win32') { platform = 'windows'; } if (!platform) { throw new Error(`${osName} is unsupported.`); } - - return platform as CliSupportedPlatform; + return platform; } static async exists(extensionDir: string, customPath?: string): Promise { diff --git a/src/snyk/cli/supportedPlatforms.ts b/src/snyk/cli/supportedPlatforms.ts index 3cef834a9..96c85fe6a 100644 --- a/src/snyk/cli/supportedPlatforms.ts +++ b/src/snyk/cli/supportedPlatforms.ts @@ -1,2 +1,11 @@ -const SupportedCliPlatformsList = ['linux', 'linux_alpine', 'windows', 'macos', 'macos_arm64'] 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 15176b287..9ac037bb4 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -321,7 +321,7 @@ export class Configuration implements IConfiguration { } async setCliPath(cliPath: string | undefined): Promise { - if (!cliPath && extensionContext) { + if (!cliPath) { cliPath = await CliExecutable.getPath(extensionContext.extensionPath); } return this.workspace.updateConfiguration( @@ -530,7 +530,7 @@ export class Configuration implements IConfiguration { CONFIGURATION_IDENTIFIER, this.getConfigName(ADVANCED_CLI_PATH), ); - if (!cliPath && extensionContext) { + if (!cliPath) { cliPath = await CliExecutable.getPath(extensionContext.extensionPath); await this.setCliPath(cliPath); } diff --git a/src/snyk/common/constants/languageServer.ts b/src/snyk/common/constants/languageServer.ts index 17b4686e8..5bb146384 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/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index 9d70afade..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, diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index 52534adfd..df83ccc42 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'; @@ -19,9 +20,11 @@ suite('CliExecutable', () => { 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'); + let expectedCliPath = path.join(unixExtensionDir, 'snyk-macos'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); @@ -29,15 +32,34 @@ suite('CliExecutable', () => { expectedCliPath = path.join(unixExtensionDir, 'snyk-linux'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); - const winExtensionDir = `C:\\Users\\user\\.vscode\\extensions`; + sinon.stub(fs, 'access').returns(Promise.resolve()); + expectedCliPath = path.join(unixExtensionDir, 'snyk-linux-alpine'); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + sinon.stub(fs, 'access').returns(Promise.reject()); + osStub.returns('win32'); expectedCliPath = path.join(winExtensionDir, 'snyk-win.exe'); strictEqual(await CliExecutable.getPath(winExtensionDir), expectedCliPath); - osStub.returns('darwin'); + // 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); + + sinon.stub(fs, 'access').returns(Promise.resolve()); + expectedCliPath = path.join(unixExtensionDir, 'snyk-linux-alpine-arm64'); + strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + sinon.stub(fs, 'access').returns(Promise.reject()); + + osStub.returns('win32'); + expectedCliPath = path.join(winExtensionDir, 'snyk-win.exe'); + strictEqual(await CliExecutable.getPath(winExtensionDir), expectedCliPath); }); test('Return custom path, if provided', async () => { From 9aa474cc822fbaed18b326d696bf4036ae67a316 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Wed, 30 Oct 2024 12:46:46 +0100 Subject: [PATCH 14/23] fix: append arch suffix --- src/snyk/cli/cliExecutable.ts | 12 ++++++------ src/test/unit/cli/cliExecutable.test.ts | 13 +++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/snyk/cli/cliExecutable.ts b/src/snyk/cli/cliExecutable.ts index 0b4d34242..b344ccbf0 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -34,14 +34,14 @@ export class CliExecutable { static async getCurrentWithArch(): Promise { const osName = Platform.getCurrent().toString().toLowerCase(); - let archSuffix = Platform.getArch().toLowerCase(); - if (archSuffix !== 'arm64') { - archSuffix = ''; - } - + const archSuffix = Platform.getArch().toLowerCase(); const platform = await this.getPlatformName(osName); - return (platform + archSuffix) as CliSupportedPlatform; + let cliName = platform; + if (archSuffix === 'arm64') { + cliName = `${platform}_${archSuffix}`; + } + return cliName as CliSupportedPlatform; } static async getPlatformName(osName: string): Promise { diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index df83ccc42..c23a946d2 100644 --- a/src/test/unit/cli/cliExecutable.test.ts +++ b/src/test/unit/cli/cliExecutable.test.ts @@ -24,6 +24,7 @@ suite('CliExecutable', () => { 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()); let expectedCliPath = path.join(unixExtensionDir, 'snyk-macos'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); @@ -32,10 +33,10 @@ suite('CliExecutable', () => { expectedCliPath = path.join(unixExtensionDir, 'snyk-linux'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); - sinon.stub(fs, 'access').returns(Promise.resolve()); - expectedCliPath = path.join(unixExtensionDir, 'snyk-linux-alpine'); + fsStub.returns(Promise.resolve()); + expectedCliPath = path.join(unixExtensionDir, 'snyk-alpine'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); - sinon.stub(fs, 'access').returns(Promise.reject()); + fsStub.returns(Promise.reject()); osStub.returns('win32'); expectedCliPath = path.join(winExtensionDir, 'snyk-win.exe'); @@ -52,10 +53,10 @@ suite('CliExecutable', () => { expectedCliPath = path.join(unixExtensionDir, 'snyk-linux-arm64'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); - sinon.stub(fs, 'access').returns(Promise.resolve()); - expectedCliPath = path.join(unixExtensionDir, 'snyk-linux-alpine-arm64'); + fsStub.returns(Promise.resolve()); + expectedCliPath = path.join(unixExtensionDir, 'snyk-alpine-arm64'); strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); - sinon.stub(fs, 'access').returns(Promise.reject()); + fsStub.returns(Promise.reject()); osStub.returns('win32'); expectedCliPath = path.join(winExtensionDir, 'snyk-win.exe'); From 5cc9c3c53daab3a8a2563f031bde2a72f2253d11 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Wed, 30 Oct 2024 12:52:46 +0100 Subject: [PATCH 15/23] fix: replace var names ls with cli --- src/snyk/common/download/downloader.ts | 8 +++---- src/snyk/common/services/downloadService.ts | 24 +++++++++---------- .../common/services/downloadService.test.ts | 18 +++++++------- src/test/unit/download/downloader.test.ts | 8 +++---- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/snyk/common/download/downloader.ts b/src/snyk/common/download/downloader.ts index a18ce9002..9a59abfa8 100644 --- a/src/snyk/common/download/downloader.ts +++ b/src/snyk/common/download/downloader.ts @@ -25,7 +25,7 @@ export class Downloader { private readonly extensionContext: ExtensionContext, ) {} /** - * Downloads LS. Existing executable is deleted. + * Downloads CLI. Existing executable is deleted. */ async download(): Promise { const platform = await CliExecutable.getCurrentWithArch(); @@ -43,8 +43,8 @@ export class Downloader { if (await this.binaryExists(cliPath)) { await this.deleteFileAtPath(cliPath); } - const lsVersion = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); - const sha256 = await this.cliApi.getSha256Checksum(lsVersion, platform); + 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) { @@ -56,7 +56,7 @@ export class Downloader { return Promise.reject(messages.integrityCheckFailed); } - return new CliExecutable(lsVersion, checksum); + return new CliExecutable(cliVersion, checksum); } private async binaryExists(filePath: string): Promise { diff --git a/src/snyk/common/services/downloadService.ts b/src/snyk/common/services/downloadService.ts index d4f16af21..1a49c4bde 100644 --- a/src/snyk/common/services/downloadService.ts +++ b/src/snyk/common/services/downloadService.ts @@ -18,22 +18,22 @@ export class DownloadService { constructor( private readonly extensionContext: ExtensionContext, private readonly configuration: IConfiguration, - private readonly lsApi: IStaticCliApi, + private readonly cliApi: IStaticCliApi, readonly window: IVSCodeWindow, private readonly logger: ILog, downloader?: Downloader, ) { - this.downloader = downloader ?? new Downloader(configuration, lsApi, window, logger, this.extensionContext); + this.downloader = downloader ?? new Downloader(configuration, cliApi, window, logger, this.extensionContext); } async downloadOrUpdate(): Promise { - const lsInstalled = await this.isCliInstalled(); + 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; @@ -60,11 +60,11 @@ export class DownloadService { async update(): Promise { const platform = await CliExecutable.getCurrentWithArch(); - const version = await this.lsApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); - const lsInstalled = await this.isCliInstalled(); + const version = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const cliInstalled = await this.isCliInstalled(); const cliVersionHasUpdated = this.hasCliVersionUpdated(version); const needsUpdate = cliVersionHasUpdated; - if (!lsInstalled || needsUpdate) { + if (!cliInstalled || needsUpdate) { const updateAvailable = await this.isCliUpdateAvailable(platform); if (!updateAvailable) { return false; @@ -83,18 +83,18 @@ export class DownloadService { } async isCliInstalled() { - const lsExecutableExists = await CliExecutable.exists( + const cliExecutableExists = await CliExecutable.exists( this.extensionContext.extensionPath, await this.configuration.getCliPath(), ); - const lsChecksumWritten = !!this.getCliChecksum(); + const cliChecksumWritten = !!this.getCliChecksum(); - return lsExecutableExists && lsChecksumWritten; + return cliExecutableExists && cliChecksumWritten; } private async isCliUpdateAvailable(platform: CliSupportedPlatform): Promise { - const version = await this.lsApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); - const latestChecksum = await this.lsApi.getSha256Checksum(version, platform); + 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(), diff --git a/src/test/unit/common/services/downloadService.test.ts b/src/test/unit/common/services/downloadService.test.ts index 7fb1a8e24..4d7526ec1 100644 --- a/src/test/unit/common/services/downloadService.test.ts +++ b/src/test/unit/common/services/downloadService.test.ts @@ -14,7 +14,7 @@ import { windowMock } from '../../mocks/window.mock'; suite('DownloadService', () => { let logger: ILog; - let lsApi: IStaticCliApi; + let cliApi: IStaticCliApi; let context: ExtensionContext; let downloader: Downloader; let configuration: IConfiguration; @@ -27,7 +27,7 @@ suite('DownloadService', () => { contextGetGlobalStateValue = sinon.stub(); apigetSha256Checksum = sinon.stub(); - lsApi = { + cliApi = { getLatestCliVersion: sinon.fake(), downloadBinary: sinon.fake(), getSha256Checksum: apigetSha256Checksum, @@ -51,20 +51,20 @@ suite('DownloadService', () => { getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; - downloader = new Downloader(configuration, lsApi, windowMock, logger, context); + 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, getCliReleaseChannel: () => 'stable', getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); + const service = new DownloadService(context, configuration, cliApi, windowMock, logger, downloader); const downloadSpy = stub(service, 'download'); const updateSpy = stub(service, 'update'); await service.downloadOrUpdate(); @@ -73,13 +73,13 @@ 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, getCliReleaseChannel: () => 'stable', getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); + 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'); @@ -90,13 +90,13 @@ suite('DownloadService', () => { strictEqual(updateSpy.calledOnce, true); }); - 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, getCliReleaseChannel: () => 'stable', getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; - const service = new DownloadService(context, configuration, lsApi, windowMock, logger, downloader); + 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'); diff --git a/src/test/unit/download/downloader.test.ts b/src/test/unit/download/downloader.test.ts index 4c3fb1fc2..4674b31e2 100644 --- a/src/test/unit/download/downloader.test.ts +++ b/src/test/unit/download/downloader.test.ts @@ -11,7 +11,7 @@ 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 cliApi: IStaticCliApi; let configuration: IConfiguration; @@ -45,13 +45,13 @@ suite('LS Downloader (LS)', () => { sinon.restore(); }); - test('Download of LS fails if platform is not supported', async () => { + 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 () => { + test('Download of CLI removes executable, if it exists', async () => { const downloader = new Downloader(configuration, cliApi, windowMock, logger, extensionContextMock); sinon.stub(CliExecutable, 'getCurrentWithArch').resolves('macos_arm64'); @@ -63,7 +63,7 @@ suite('LS Downloader (LS)', () => { strictEqual(unlink.calledOnceWith(cliPath), true); }); - test('Rejects downloaded LS when integrity check fails', async () => { + 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); From cb5c818c1320bcf45697906f409f225c8807df61 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Thu, 7 Nov 2024 13:29:43 +0100 Subject: [PATCH 16/23] fix: fallback to snyk-ls path --- src/snyk/cli/cliExecutable.ts | 5 +++- .../common/configuration/configuration.ts | 23 ++++++++++++++++++- src/snyk/common/constants/settings.ts | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/snyk/cli/cliExecutable.ts b/src/snyk/cli/cliExecutable.ts index b344ccbf0..2f8dac8a4 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -18,8 +18,11 @@ export class CliExecutable { constructor(public readonly version: string, public readonly checksum: Checksum) {} static async getPath(extensionDir: string, customPath?: string): Promise { - // check if custom path is file if (customPath) { + const stats = await fs.stat(customPath); + if (stats.isDirectory()) { + throw new Error('CLI custom path is a directory.'); + } return customPath; } diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 9ac037bb4..15a7d5d78 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -12,6 +12,7 @@ import { ADVANCED_CLI_PATH, ADVANCED_CLI_RELEASE_CHANNEL, ADVANCED_CUSTOM_ENDPOINT, + ADVANCED_CUSTOM_LS_PATH, ADVANCED_ORGANIZATION, CODE_QUALITY_ENABLED_SETTING, CODE_SECURITY_ENABLED_SETTING, @@ -132,6 +133,8 @@ export interface IConfiguration { scanningMode: string | undefined; + getSnykLanguageServerPath(): string | undefined; + getTrustedFolders(): string[]; setTrustedFolders(trustedFolders: string[]): Promise; @@ -197,6 +200,13 @@ export class Configuration implements IConfiguration { 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; @@ -531,12 +541,23 @@ export class Configuration implements IConfiguration { this.getConfigName(ADVANCED_CLI_PATH), ); if (!cliPath) { - cliPath = await CliExecutable.getPath(extensionContext.extensionPath); + 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 defaultPath = await CliExecutable.getPath(extensionContext.extensionPath); + const isAutomaticDependencyManagementEnabled = this.isAutomaticDependencyManagementEnabled(); + const snykLsPath = this.getSnykLanguageServerPath(); + if(!isAutomaticDependencyManagementEnabled && snykLsPath) + return snykLsPath; + return defaultPath; + } getTrustedFolders(): string[] { return ( this.workspace.getConfiguration(CONFIGURATION_IDENTIFIER, this.getConfigName(TRUSTED_FOLDERS)) || [] diff --git a/src/snyk/common/constants/settings.ts b/src/snyk/common/constants/settings.ts index 040d1b504..f2f9e2d28 100644 --- a/src/snyk/common/constants/settings.ts +++ b/src/snyk/common/constants/settings.ts @@ -19,6 +19,7 @@ export const ADVANCED_CUSTOM_ENDPOINT = `${CONFIGURATION_IDENTIFIER}.advanced.cu export const ADVANCED_ORGANIZATION = `${CONFIGURATION_IDENTIFIER}.advanced.organization`; 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`; From 77b156eb6db9b8818a5389f0affe6333034b8737 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Thu, 7 Nov 2024 14:27:14 +0100 Subject: [PATCH 17/23] fix: test cli path determination --- src/snyk/cli/cliExecutable.ts | 4 -- .../common/configuration/configuration.ts | 2 +- src/test/unit/common/configuration.test.ts | 41 ++++++++++++++++--- src/test/unit/mocks/extensionContext.mock.ts | 2 +- src/test/unit/mocks/workspace.mock.ts | 3 ++ 5 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/snyk/cli/cliExecutable.ts b/src/snyk/cli/cliExecutable.ts index 2f8dac8a4..3bfe289b5 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -19,10 +19,6 @@ export class CliExecutable { static async getPath(extensionDir: string, customPath?: string): Promise { if (customPath) { - const stats = await fs.stat(customPath); - if (stats.isDirectory()) { - throw new Error('CLI custom path is a directory.'); - } return customPath; } diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 15a7d5d78..b17030096 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -551,11 +551,11 @@ export class Configuration implements IConfiguration { // 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 defaultPath = await CliExecutable.getPath(extensionContext.extensionPath); const isAutomaticDependencyManagementEnabled = this.isAutomaticDependencyManagementEnabled(); const snykLsPath = this.getSnykLanguageServerPath(); if(!isAutomaticDependencyManagementEnabled && snykLsPath) return snykLsPath; + const defaultPath = await CliExecutable.getPath(extensionContext.extensionPath); return defaultPath; } getTrustedFolders(): string[] { diff --git a/src/test/unit/common/configuration.test.ts b/src/test/unit/common/configuration.test.ts index 450e9271d..1d89272fb 100644 --- a/src/test/unit/common/configuration.test.ts +++ b/src/test/unit/common/configuration.test.ts @@ -4,28 +4,28 @@ import { deepStrictEqual, strictEqual } from 'assert'; import sinon from 'sinon'; import { Configuration, PreviewFeatures } from '../../../snyk/common/configuration/configuration'; 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'; 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; @@ -137,5 +137,34 @@ suite('Configuration', () => { strictEqual(configuration.isFedramp, false); }); + + test('CLI Path: Returns default path if empty', async () => { + let 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(); + strictEqual(cliPath, 'path/to/extension/snyk-linux'); + }); + + test('CLI Path: Returns Snyk LS path', async () => { + let 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 () => { + let 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/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; } From 445dd02abe4b65fd7ca2da4106c1b845f06d50bb Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Thu, 7 Nov 2024 14:27:56 +0100 Subject: [PATCH 18/23] chore: lint --- src/snyk/common/configuration/configuration.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index b17030096..d33e3737e 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -548,15 +548,14 @@ export class Configuration implements IConfiguration { } 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; + // 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 ( From 8c95e9f226db1765093f11e18ff8ea247cb12175 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Thu, 7 Nov 2024 15:02:02 +0100 Subject: [PATCH 19/23] chore: lint --- src/test/unit/common/configuration.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/unit/common/configuration.test.ts b/src/test/unit/common/configuration.test.ts index 1d89272fb..93572bd41 100644 --- a/src/test/unit/common/configuration.test.ts +++ b/src/test/unit/common/configuration.test.ts @@ -139,7 +139,7 @@ suite('Configuration', () => { }); test('CLI Path: Returns default path if empty', async () => { - let workspace = stubWorkspaceConfiguration(ADVANCED_CLI_PATH, ''); + const workspace = stubWorkspaceConfiguration(ADVANCED_CLI_PATH, ''); const configuration = new Configuration({}, workspace); sinon.stub(Platform, 'getCurrent').returns('linux'); @@ -150,7 +150,7 @@ suite('Configuration', () => { }); test('CLI Path: Returns Snyk LS path', async () => { - let workspace = stubWorkspaceConfiguration(ADVANCED_CUSTOM_LS_PATH, '/path/to/ls'); + const workspace = stubWorkspaceConfiguration(ADVANCED_CUSTOM_LS_PATH, '/path/to/ls'); const configuration = new Configuration({}, workspace); @@ -159,7 +159,7 @@ suite('Configuration', () => { }); test('CLI Path: Returns CLI Path if set', async () => { - let workspace = stubWorkspaceConfiguration(ADVANCED_CLI_PATH, '/path/to/cli'); + const workspace = stubWorkspaceConfiguration(ADVANCED_CLI_PATH, '/path/to/cli'); const configuration = new Configuration({}, workspace); From a1b076430f7ee4ef7162d9f5fb04950bf9c6fc19 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Thu, 7 Nov 2024 15:06:32 +0100 Subject: [PATCH 20/23] fix: path test for windows --- src/test/unit/common/configuration.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/unit/common/configuration.test.ts b/src/test/unit/common/configuration.test.ts index 93572bd41..bfbe7c7b7 100644 --- a/src/test/unit/common/configuration.test.ts +++ b/src/test/unit/common/configuration.test.ts @@ -16,6 +16,7 @@ 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; @@ -146,7 +147,9 @@ suite('Configuration', () => { sinon.stub(Platform, 'getArch').returns('x64'); const cliPath = await configuration.getCliPath(); - strictEqual(cliPath, 'path/to/extension/snyk-linux'); + + const expectedCliPath = path.join('path/to/extension/', 'snyk-linux'); + strictEqual(cliPath, expectedCliPath); }); test('CLI Path: Returns Snyk LS path', async () => { From dfed2ff49c540e2e7fdc7cb9d91238597c6a443c Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Mon, 11 Nov 2024 15:27:30 +0100 Subject: [PATCH 21/23] chore: clear unused imports --- src/snyk/extension.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index 354ad27a4..e54d7654c 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -48,8 +48,7 @@ import { NotificationService } from './common/services/notificationService'; import { User } from './common/user'; import { CodeActionAdapter } from './common/vscode/codeAction'; import { vsCodeCommands } from './common/vscode/commands'; -import { vsCodeEnv } from './common/vscode/env'; -import { ExtensionContext, extensionContext } from './common/vscode/extensionContext'; +import { extensionContext } from './common/vscode/extensionContext'; import { LanguageClientAdapter } from './common/vscode/languageClient'; import { vsCodeLanguages } from './common/vscode/languages'; import SecretStorageAdapter from './common/vscode/secretStorage'; From 7f6ee6cb4243fb17e6432f3ea59ee626108ade80 Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Mon, 11 Nov 2024 15:33:45 +0100 Subject: [PATCH 22/23] fix: protcol version memento --- src/snyk/common/constants/globalState.ts | 1 + src/snyk/common/services/downloadService.ts | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/snyk/common/constants/globalState.ts b/src/snyk/common/constants/globalState.ts index fb4e6de24..4d8add9da 100644 --- a/src/snyk/common/constants/globalState.ts +++ b/src/snyk/common/constants/globalState.ts @@ -1,4 +1,5 @@ export const MEMENTO_ANONYMOUS_ID = 'snyk.anonymousId'; 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_ANALYTICS_PLUGIN_INSTALLED_SENT = 'snyk.pluginInstalledSent'; diff --git a/src/snyk/common/services/downloadService.ts b/src/snyk/common/services/downloadService.ts index 1a49c4bde..5834cae53 100644 --- a/src/snyk/common/services/downloadService.ts +++ b/src/snyk/common/services/downloadService.ts @@ -2,7 +2,7 @@ import { ReplaySubject } from 'rxjs'; import { Checksum } from '../../cli/checksum'; import { messages } from '../../cli/messages/messages'; import { IConfiguration } from '../configuration/configuration'; -import { MEMENTO_CLI_CHECKSUM, MEMENTO_CLI_VERSION } from '../constants/globalState'; +import { MEMENTO_CLI_CHECKSUM, MEMENTO_CLI_VERSION, MEMENTO_LS_PROTOCOL_VERSION } from '../constants/globalState'; import { Downloader } from '../download/downloader'; import { CliExecutable } from '../../cli/cliExecutable'; import { IStaticCliApi } from '../../cli/staticCliApi'; @@ -10,6 +10,7 @@ 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 downloadReady$ = new ReplaySubject(1); @@ -63,7 +64,7 @@ export class DownloadService { const version = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); const cliInstalled = await this.isCliInstalled(); const cliVersionHasUpdated = this.hasCliVersionUpdated(version); - const needsUpdate = cliVersionHasUpdated; + const needsUpdate = cliVersionHasUpdated || this.hasLspVersionUpdated(); if (!cliInstalled || needsUpdate) { const updateAvailable = await this.isCliUpdateAvailable(platform); if (!updateAvailable) { @@ -76,6 +77,7 @@ export class DownloadService { await this.setCliChecksum(executable.checksum); await this.setCliVersion(executable.version); + await this.setCurrentLspVersion(); this.logger.info(messages.downloadFinished(executable.version)); return true; } @@ -118,6 +120,19 @@ export class DownloadService { await this.extensionContext.updateGlobalStateValue(MEMENTO_CLI_VERSION, cliVersion); } + private hasLspVersionUpdated(): boolean { + const currentProtoclVersion = this.getLsProtocolVersion(); + return currentProtoclVersion != PROTOCOL_VERSION; + } + + 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 hasCliVersionUpdated(cliVersion: string): boolean { const currentVersion = this.getCliVersion(); return currentVersion != cliVersion; From d82fe946a8e848d4a89049fe074a7b22a77ee55a Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Mon, 11 Nov 2024 15:38:37 +0100 Subject: [PATCH 23/23] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7019f043..2e1100a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - 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