diff --git a/.vscode/launch.json b/.vscode/launch.json index 6a33081f..356d879b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -75,7 +75,7 @@ "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "args": [ "--timeout", - "0" + "987654" ], "internalConsoleOptions": "openOnSessionStart" } @@ -104,4 +104,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index bfac465b..8c4a4f2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "@types/fs-extra": "^5.0.4", "@types/glob": "^7.1.1", "@types/mocha": "^7.0.2", - "@types/node": "^12.12.0", + "@types/node": "^20.14.10", "@types/node-ssdp": "^3.3.0", "@types/prompt": "^1.1.2", "@types/resolve": "^1.20.6", @@ -1467,9 +1467,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/node-ssdp": { "version": "3.3.1", @@ -10542,6 +10545,11 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", diff --git a/package.json b/package.json index 3bfbf516..c08e8ff6 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "@types/fs-extra": "^5.0.4", "@types/glob": "^7.1.1", "@types/mocha": "^7.0.2", - "@types/node": "^12.12.0", + "@types/node": "^20.14.10", "@types/node-ssdp": "^3.3.0", "@types/prompt": "^1.1.2", "@types/resolve": "^1.20.6", diff --git a/src/LanguageServerManager.spec.ts b/src/LanguageServerManager.spec.ts index 581cfbd6..d17e6b82 100644 --- a/src/LanguageServerManager.spec.ts +++ b/src/LanguageServerManager.spec.ts @@ -15,6 +15,7 @@ import { LanguageClient, State } from 'vscode-languageclient/node'; +import * as childProcess from 'child_process'; const Module = require('module'); const sinon = createSandbox(); @@ -75,7 +76,7 @@ describe('LanguageServerManager', () => { //disable starting so we can manually test sinon.stub(languageServerManager, 'syncVersionAndTryRun').callsFake(() => Promise.resolve()); - await languageServerManager.init(languageServerManager['context'], languageServerManager['definitionRepository']); + await languageServerManager.init(languageServerManager['context'], languageServerManager['definitionRepository'], languageServerManager['logger']); languageServerManager['lspRunTracker'].debounceDelay = 100; @@ -323,4 +324,58 @@ describe('LanguageServerManager', () => { expect(bsdkPath).to.eql(null); }); }); + + describe.only('ensureBscVersionInstalled', function() { + //these tests take a long time (due to running `npm install`) + this.timeout(20_000); + + const storageDir = s`${tempDir}/brighterscript-storage`; + beforeEach(() => { + fsExtra.removeSync(storageDir); + (languageServerManager['context'] as any).globalStorageUri = URI.file(storageDir); + }); + + it('installs a bsc version when not present', async () => { + expect( + await languageServerManager['ensureBscVersionInstalled']('0.65.0') + ).to.eql(s`${storageDir}/packages/brighterscript-0.65.0/node_modules/brighterscript`); + expect( + fsExtra.pathExistsSync(s`${storageDir}/packages/brighterscript-0.65.0/node_modules/brighterscript`) + ).to.be.true; + }); + + it('reuses the same bsc version when already exists', async () => { + let stub = sinon.stub(childProcess, 'exec'); + expect( + await languageServerManager['ensureBscVersionInstalled']('0.65.0') + ).to.eql(s`${storageDir}/packages/brighterscript-0.65.0/node_modules/brighterscript`); + expect( + fsExtra.pathExistsSync(s`${storageDir}/packages/brighterscript-0.65.0/node_modules/brighterscript`) + ).to.be.true; + expect(stub.called).to.be.false; + }); + + it('repairs a broken bsc version', async () => { + let stub = sinon.stub(fsExtra, 'remove'); + fsExtra.ensureDirSync( + s`${storageDir}/packages/brighterscript-0.65.1/node_modules/brighterscript` + ); + fsExtra.writeFileSync( + s`${storageDir}/packages/brighterscript-0.65.1/node_modules/brighterscript/package.json`, + 'bad json' + ); + + expect( + await languageServerManager['ensureBscVersionInstalled']('0.65.1') + ).to.eql(s`${storageDir}/packages/brighterscript-0.65.1/node_modules/brighterscript`); + expect( + fsExtra.pathExistsSync(s`${storageDir}/packages/brighterscript-0.65.1/node_modules/brighterscript`) + ).to.be.true; + + //make sure we deleted the bad folder + expect( + s`${stub.getCalls()[0].args[0]}` + ).to.eql(s`${storageDir}/packages/brighterscript-0.65.1`); + }); + }); }); diff --git a/src/LanguageServerManager.ts b/src/LanguageServerManager.ts index 9e264bff..f46dae0b 100644 --- a/src/LanguageServerManager.ts +++ b/src/LanguageServerManager.ts @@ -16,7 +16,8 @@ import { window, workspace } from 'vscode'; -import { BusyStatus, NotificationName, Logger } from 'brighterscript'; +import { BusyStatus, NotificationName, standardizePath as s } from 'brighterscript'; +import { Logger } from '@rokucommunity/logger'; import { CustomCommands, Deferred } from 'brighterscript'; import type { CodeWithSourceMap } from 'source-map'; import BrightScriptDefinitionProvider from './BrightScriptDefinitionProvider'; @@ -29,6 +30,7 @@ import { util } from './util'; import { LanguageServerInfoCommand, languageServerInfoCommand } from './commands/LanguageServerInfoCommand'; import * as fsExtra from 'fs-extra'; import { EventEmitter } from 'eventemitter3'; +import * as childProcess from 'child_process'; /** * Tracks the running/stopped state of the language server. When the lsp crashes, vscode will restart it. After the 5th crash, they'll leave it permanently crashed. @@ -88,10 +90,13 @@ export class LanguageServerManager { return this.definitionRepository.provider; } + private logger: Logger; public async init( context: vscode.ExtensionContext, - definitionRepository: DefinitionRepository + definitionRepository: DefinitionRepository, + logger: Logger ) { + this.logger = logger; this.context = context; this.definitionRepository = definitionRepository; @@ -266,7 +271,7 @@ export class LanguageServerManager { if (event.status === BusyStatus.busy) { timeoutHandle = setTimeout(() => { const delay = Date.now() - event.timestamp; - this.client.outputChannel.appendLine(`${logger.getTimestamp()} language server has been 'busy' for ${delay}ms. most recent busyStatus event: ${JSON.stringify(event, undefined, 4)}`); + this.client.outputChannel.appendLine(`${logger.formatTimestamp(new Date())} language server has been 'busy' for ${delay}ms. most recent busyStatus event: ${JSON.stringify(event, undefined, 4)}`); }, 60_000); //clear any existing timeout @@ -386,6 +391,7 @@ export class LanguageServerManager { * and if different, re-launch the specific version of the language server' */ public async syncVersionAndTryRun() { + await this.ensureBscVersionInstalled('0.67.3'); const bsdkPath = await this.getBsdkPath(); //if the path to bsc is different, spin down the old server and start a new one @@ -460,6 +466,53 @@ export class LanguageServerManager { ).toString() ); } + + /** + * Ensure that the specified bsc version is installed in the global storage directory. + * @param version + * @param retryCount the number of times we should retry before giving up + * @returns full path to the root of where the brighterscript module is installed + */ + private async ensureBscVersionInstalled(version: string, retryCount = 1) { + console.log('Ensuring bsc version is installed', version); + const bscNpmDir = s`${this.context.globalStorageUri.fsPath}/packages/brighterscript-${version}`; + if (await fsExtra.pathExists(bscNpmDir) === false) { + //write a simple package.json file referencing the version of brighterscript we want + await fsExtra.outputJson(`${bscNpmDir}/package.json`, { + name: 'vscode-brighterscript-host', + private: true, + version: '1.0.0', + dependencies: { + 'brighterscript': version + } + }); + await new Promise((resolve, reject) => { + const process = childProcess.exec(`npm install`, { + cwd: bscNpmDir + }); + process.on('error', (err) => { + console.error(err); + reject(err); + }); + process.on('close', (code) => { + if (code === 0) { + resolve(); + } + }); + }); + } + const bscPath = s`${bscNpmDir}/node_modules/brighterscript`; + + //if the module is invalid, try again + if (await fsExtra.pathExists(`${bscPath}/dist/index.js`) === false && retryCount > 0) { + console.log(`Failed to load brighterscript module at ${bscNpmDir}. Deleting directory and trying again`); + //remove the dir and try again + await fsExtra.remove(bscNpmDir); + return this.ensureBscVersionInstalled(version, retryCount - 1); + } + + return bscPath; + } } export const languageServerManager = new LanguageServerManager();