@@ -7,14 +7,16 @@ import path from 'path'
77import * as crypto from 'crypto'
88import fs from './fs/fs'
99import { getLogger } from './logger/logger'
10- import request from './request'
1110import { getUserAgent } from './telemetry/util'
1211import { ToolkitError } from './errors'
1312import fetch from 'node-fetch'
1413// TODO remove
1514// eslint-disable-next-line no-restricted-imports
1615import { createWriteStream } from 'fs'
1716import AdmZip from 'adm-zip'
17+ import { RetryableResourceFetcher } from './resourcefetcher/httpResourceFetcher'
18+ import { Timeout } from './utilities/timeoutUtils'
19+ import globals from './extensionGlobals'
1820
1921export interface Content {
2022 filename : string
@@ -42,25 +44,58 @@ export interface Manifest {
4244 } [ ]
4345}
4446
47+ export const logger = getLogger ( 'lsp' )
48+
49+ interface StorageManifest {
50+ etag : string
51+ content : string
52+ }
53+
4554export abstract class LspDownloader {
4655 constructor (
4756 private readonly manifestURL : string ,
57+ protected readonly lsName : string ,
4858 private readonly supportedLspServerVersions ?: string [ ]
4959 ) { }
5060
51- async fetchManifest ( ) {
61+ /**
62+ * Finds the latest available manifest. If it fails to download then fallback to the local
63+ * manifest version
64+ */
65+ async downloadManifest ( ) {
5266 try {
53- const resp = await request . fetch ( 'GET' , this . manifestURL , {
54- headers : {
55- 'User-Agent' : getUserAgent ( { includePlatform : true , includeClientId : true } ) ,
67+ const resourceFetcher = new RetryableResourceFetcher ( {
68+ resource : this . manifestURL ,
69+ params : {
70+ timeout : new Timeout ( 15000 ) ,
5671 } ,
57- } ) . response
58- if ( ! resp . ok ) {
59- throw new ToolkitError ( `Failed to fetch manifest. Error: ${ resp . statusText } ` )
72+ } )
73+ const etag = ( globals . globalState . tryGet ( 'aws.toolkit.lsp.manifest' , Object ) as StorageManifest ) ?. etag
74+ const resp = await resourceFetcher . getNewETagContent ( etag )
75+ if ( resp . content === undefined ) {
76+ throw new ToolkitError ( 'Content was not downloaded' )
77+ }
78+
79+ const manifest = JSON . parse ( resp . content ) as Manifest
80+ if ( manifest . isManifestDeprecated ) {
81+ logger . info ( 'This LSP manifest is deprecated. No future updates will be available.' )
6082 }
61- return resp . json ( )
83+ globals . globalState . tryUpdate ( 'aws.toolkit.lsp.manifest' , {
84+ etag : resp . eTag ,
85+ content : resp . content ,
86+ } as StorageManifest )
87+ return manifest
6288 } catch ( e : any ) {
63- throw new ToolkitError ( `Failed to fetch manifest. Error: ${ JSON . stringify ( e ) } ` )
89+ logger . info ( 'Failed to download latest LSP manifest. Falling back to local manifest.' )
90+ const manifest = globals . globalState . tryGet ( 'aws.toolkit.lsp.manifest' , Object )
91+ if ( ! manifest ) {
92+ throw new ToolkitError ( 'Failed to download LSP manifest and no local manifest found.' )
93+ }
94+
95+ if ( manifest ?. isManifestDeprecated ) {
96+ logger . info ( 'This LSP manifest is deprecated. No future updates will be available.' )
97+ }
98+ return manifest
6499 }
65100 }
66101
@@ -95,7 +130,7 @@ export abstract class LspDownloader {
95130 private async hashMatch ( filePath : string , content : Content ) {
96131 const sha384 = await this . getFileSha384 ( filePath )
97132 if ( 'sha384:' + sha384 !== content . hashes [ 0 ] ) {
98- getLogger ( 'lsp' ) . error ( `Downloaded file sha ${ sha384 } does not match manifest ${ content . hashes [ 0 ] } .` )
133+ logger . error ( `Downloaded file sha ${ sha384 } does not match manifest ${ content . hashes [ 0 ] } .` )
99134 await fs . delete ( filePath )
100135 return false
101136 }
@@ -104,11 +139,7 @@ export abstract class LspDownloader {
104139
105140 async downloadAndCheckHash ( filePath : string , content : Content ) {
106141 await this . _download ( filePath , content . url )
107- const match = await this . hashMatch ( filePath , content )
108- if ( ! match ) {
109- return false
110- }
111- return true
142+ return await this . hashMatch ( filePath , content )
112143 }
113144
114145 getDependency ( manifest : Manifest , name : string ) : Content | undefined {
@@ -189,26 +220,117 @@ export abstract class LspDownloader {
189220 */
190221 abstract install ( manifest : Manifest ) : Promise < boolean >
191222
223+ /**
224+ * Get the currently installed version of the LSP on disk
225+ */
226+ async latestInstalledVersion ( ) : Promise < string | undefined > {
227+ const latestVersion : Record < string , string > = globals . globalState . tryGet ( 'aws.toolkit.lsp.versions' , Object )
228+ if ( ! latestVersion || ! latestVersion [ this . lsName ] ) {
229+ return undefined
230+ }
231+ return latestVersion [ this . lsName ]
232+ }
233+
192234 async tryInstallLsp ( ) : Promise < boolean > {
193235 try {
194- if ( await this . isLspInstalled ( ) ) {
195- getLogger ( 'lsp' ) . info ( `LSP already installed ` )
236+ if ( process . env . AWS_LANGUAGE_SERVER_OVERRIDE ) {
237+ logger . info ( `LSP override location: ${ process . env . AWS_LANGUAGE_SERVER_OVERRIDE } ` )
196238 return true
197239 }
198240
199- const clean = await this . cleanup ( )
200- if ( ! clean ) {
201- getLogger ( 'lsp' ) . error ( `Failed to clean up old LSPs` )
202- return false
203- }
241+ const manifest : Manifest = await this . downloadManifest ( )
204242
205- // fetch download url for server and runtime
206- const manifest : Manifest = ( await this . fetchManifest ( ) ) as Manifest
243+ // if a compatible version was found check what's installed locally
244+ if ( this . latestCompatibleVersion ( manifest ) ) {
245+ return await this . checkInstalledLS ( manifest )
246+ }
207247
208- return await this . install ( manifest )
209- } catch ( e ) {
210- getLogger ( ) . error ( `LspController: Failed to setup LSP server ${ e } ` )
248+ // we found no latest compatible version in the manifest; try to fallback to a local version
249+ return this . fallbackToLocalVersion ( manifest )
250+ } catch ( err ) {
251+ const e = err as ToolkitError
252+ logger . info ( `Failed to setup LSP server: ${ e . message } ` )
211253 return false
212254 }
213255 }
256+
257+ /**
258+ * Attempts to fall back to a local version if one is available
259+ */
260+ async fallbackToLocalVersion ( manifest ?: Manifest ) : Promise < boolean > {
261+ // was language server previously downloaded?
262+ const installed = await this . isLspInstalled ( )
263+
264+ // yes
265+ if ( installed ) {
266+ if ( ! manifest ) {
267+ // we want to launch if the manifest can't be found
268+ return true
269+ }
270+
271+ // the manifest is found; check that the current version is not delisted
272+ const currentVersion = await this . latestInstalledVersion ( )
273+ const v = manifest . versions . find ( ( v ) => v . serverVersion === currentVersion )
274+ if ( v ?. isDelisted ) {
275+ throw new ToolkitError ( 'Local LSP version is delisted. Please update to a newer version.' )
276+ }
277+
278+ // current version is not delisted, we should launch
279+ return true
280+ }
281+
282+ // it was not installed before
283+ throw new ToolkitError ( 'No compatible local LSP version found' , { code : 'LSPNotInstalled' } )
284+ }
285+
286+ /**
287+ * Check to see if we can re-use the previously downloaded language server.
288+ * If it wasn't previously downloaded then download it and store it
289+ * If it was then check the current installed language version
290+ * If there is an error, download the latest version and store it
291+ * If there wasn't an error, compare the current and latest versions
292+ * If they mismatch then download the latest language server and store it
293+ * If they are the same then launch the language server
294+ */
295+ async checkInstalledLS ( manifest : Manifest ) : Promise < boolean > {
296+ // was ls previously downloaded
297+ const installed = await this . isLspInstalled ( )
298+
299+ // yes
300+ if ( installed ) {
301+ try {
302+ const currentVersion = await this . latestInstalledVersion ( )
303+ if ( currentVersion !== this . latestCompatibleVersion ( manifest ) ) {
304+ // download and install latest version
305+ return this . _install ( manifest )
306+ }
307+ return true
308+ } catch ( e ) {
309+ logger . info ( 'Failed to query language server for installed version' )
310+
311+ // error found! download the latest version and store it
312+ return this . _install ( manifest )
313+ }
314+ }
315+
316+ // no; install and store it
317+ return this . _install ( manifest )
318+ }
319+
320+ private _install ( manifest : Manifest ) : Promise < boolean > {
321+ return this . install ( manifest ) . catch ( ( _ ) => this . fallbackToLocalVersion ( manifest ) )
322+ }
323+
324+ private latestCompatibleVersion ( manifest : Manifest ) {
325+ for ( const version of manifest . versions ) {
326+ if ( version . isDelisted ) {
327+ continue
328+ }
329+ if ( this . supportedLspServerVersions && ! this . supportedLspServerVersions . includes ( version . serverVersion ) ) {
330+ continue
331+ }
332+ return version . serverVersion
333+ }
334+ return undefined
335+ }
214336}
0 commit comments