From 779bbd2df4e19c77aa81443ffc28a37c28cf3ae0 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 8 Sep 2023 14:14:00 +0200 Subject: [PATCH] feat: add detection for standalone/cli-embedded based on size, add logging args if given `-d` or env var - SNYK_LOG_DEVEL overrules all log level settings now - `-d` or `--debug` in additional parameters now puts language server in debug mode Signed-off-by: Bastian Doetsch --- CHANGELOG.md | 5 +- .../common/languageServer/languageServer.ts | 27 +++- .../languageServer/languageServer.test.ts | 134 +++++++++++++++++- 3 files changed, 157 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f91212396..9ff6411a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Snyk Security - Code and Open Source Dependencies Changelog -## [1.21.6] +## [1.23.0] +- add detection for standalone/cli embedded language server based on binary size + +## [1.22.0] ### Added diff --git a/src/snyk/common/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index b9ca37a04..7d6edca88 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -24,10 +24,13 @@ import { LsExecutable } from './lsExecutable'; import { LanguageClientMiddleware } from './middleware'; import { InitializationOptions, LanguageServerSettings } from './settings'; import { CodeIssueData, IacIssueData, OssIssueData, Scan } from './types'; +import * as fs from 'fs'; export interface ILanguageServer { start(): Promise; + stop(): Promise; + showOutputChannel(): void; cliReady$: ReplaySubject; @@ -77,11 +80,31 @@ export class LanguageServer implements ILanguageServer { const lsBinaryPath = LsExecutable.getPath(this.configuration.getSnykLanguageServerPath()); - this.logger.info(`Snyk Language Server path: ${lsBinaryPath}`); + // log level is set to info by default + let logLevel = 'info'; + const additionalCliParameters = this.configuration.getAdditionalCliParameters(); + if ( + additionalCliParameters != null && + additionalCliParameters.length > 0 && + (additionalCliParameters.includes('-d') || additionalCliParameters.includes('--debug')) + ) { + logLevel = 'debug'; + } + logLevel = process.env.SNYK_LOG_LEVEL ?? logLevel; + + // check size of file at lsBinaryPath to determine if cli or ls + let args = ['-l', logLevel]; + const lsBinarySize = fs.statSync(lsBinaryPath).size; + const fiftyMB = 50 * 1024 * 1024; + if (lsBinarySize > fiftyMB) { + args = ['language-server', ...args]; + } + this.logger.info(`Snyk Language Server - path: ${lsBinaryPath}`); + this.logger.info(`Snyk Language Server - args: ${args}`); const serverOptions: ServerOptions = { command: lsBinaryPath, - args: ['-l', 'info'], + args: args, options: { env: processEnv, }, diff --git a/src/test/unit/common/languageServer/languageServer.test.ts b/src/test/unit/common/languageServer/languageServer.test.ts index 8c79cc50e..3f4fc43af 100644 --- a/src/test/unit/common/languageServer/languageServer.test.ts +++ b/src/test/unit/common/languageServer/languageServer.test.ts @@ -1,5 +1,5 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import assert, { deepStrictEqual, strictEqual } from 'assert'; +/* eslint-disable @typescript-eslint/no-empty-function */ +import assert, { deepStrictEqual, fail, strictEqual } from 'assert'; import { ReplaySubject } from 'rxjs'; import sinon from 'sinon'; import { v4 } from 'uuid'; @@ -16,6 +16,7 @@ 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 * as fs from 'fs'; suite('Language Server', () => { const authServiceMock = {} as IAuthenticationService; @@ -24,13 +25,23 @@ suite('Language Server', () => { let configurationMock: IConfiguration; let languageServer: LanguageServer; let downloadServiceMock: DownloadService; + const path = 'testPath'; + const logger = { + info(_msg: string) {}, + warn(_msg: string) {}, + log(_msg: string) {}, + error(msg: string) { + fail(msg); + }, + } as unknown as LoggerMock; + setup(() => { configurationMock = { getInsecure(): boolean { return true; }, getCliPath(): string | undefined { - return 'testPath'; + return path; }, getToken(): Promise { return Promise.resolve('testToken'); @@ -38,10 +49,15 @@ suite('Language Server', () => { shouldReportEvents: true, shouldReportErrors: true, getSnykLanguageServerPath(): string { - return 'testPath'; + try { + fs.statSync(path); + } catch (_) { + fs.writeFileSync(path, 'huhu'); + } + return path; }, getAdditionalCliParameters() { - return '--all-projects'; + return '--all-projects -d'; }, isAutomaticDependencyManagementEnabled() { return true; @@ -73,9 +89,114 @@ suite('Language Server', () => { }); teardown(() => { + fs.rmSync(path, { force: true }); sinon.restore(); }); + test('LanguageServer (standalone) starts with correct args', async () => { + const lca = sinon.spy({ + create( + _id: string, + _name: string, + serverOptions: ServerOptions, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _clientOptions: LanguageClientOptions, + ): LanguageClient { + return { + start(): Promise { + assert.strictEqual('args' in serverOptions ? serverOptions?.args?.[0] : '', '-l'); + assert.strictEqual('args' in serverOptions ? serverOptions?.args?.[1] : '', 'debug'); + return Promise.resolve(); + }, + onNotification(): void { + return; + }, + onReady(): Promise { + return Promise.resolve(); + }, + } as unknown as LanguageClient; + }, + }); + + languageServer = new LanguageServer( + user, + configurationMock, + lca as unknown as ILanguageClientAdapter, + stubWorkspaceConfiguration('snyk.loglevel', 'trace'), + windowMock, + authServiceMock, + { + info(_msg: string) {}, + warn(_msg: string) {}, + log(_msg: string) {}, + error(msg: string) { + fail(msg); + }, + } as unknown as LoggerMock, + downloadServiceMock, + ); + downloadServiceMock.downloadReady$.next(); + + await languageServer.start(); + sinon.assert.called(lca.create); + sinon.verify(); + }); + + test('LanguageServer (embedded) starts with language-server arg', async () => { + configurationMock.getSnykLanguageServerPath = () => { + try { + fs.statSync(path); + } catch (_) { + // write >50MB of data to simulate a CLI sized binary + let data = ''; + const size = 55 * 128 * 1024; // 128 * 8 = 1024 bytes, 55 * 1024 * 1024 = 55MB + for (let i = 0; i < size; i++) { + data += '01234567'; + } + fs.writeFileSync(path, data); + } + return path; + }; + const lca = sinon.spy({ + create( + _id: string, + _name: string, + serverOptions: ServerOptions, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _clientOptions: LanguageClientOptions, + ): LanguageClient { + return { + start(): Promise { + assert.strictEqual('args' in serverOptions ? serverOptions?.args?.[0] : '', 'language-server'); + return Promise.resolve(); + }, + onNotification(): void { + return; + }, + onReady(): Promise { + return Promise.resolve(); + }, + } as unknown as LanguageClient; + }, + }); + + languageServer = new LanguageServer( + user, + configurationMock, + lca as unknown as ILanguageClientAdapter, + stubWorkspaceConfiguration('snyk.loglevel', 'trace'), + windowMock, + authServiceMock, + logger, + downloadServiceMock, + ); + downloadServiceMock.downloadReady$.next(); + + await languageServer.start(); + sinon.assert.called(lca.create); + sinon.verify(); + }); + test('LanguageServer adds proxy settings to env of started binary', async () => { const expectedProxy = 'http://localhost:8080'; const lca = sinon.spy({ @@ -90,9 +211,11 @@ suite('Language Server', () => { assert.strictEqual(id, 'Snyk LS'); assert.strictEqual(name, 'Snyk Language Server'); assert.strictEqual( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 'options' in serverOptions ? serverOptions?.options?.env?.http_proxy : undefined, expectedProxy, ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access assert.strictEqual(clientOptions.initializationOptions.token, 'testToken'); return Promise.resolve(); }, @@ -117,7 +240,6 @@ suite('Language Server', () => { downloadServiceMock, ); downloadServiceMock.downloadReady$.next(); - await languageServer.start(); sinon.assert.called(lca.create); sinon.verify(); });