@@ -3,12 +3,14 @@ import * as fs from 'fs';
3
3
import * as https from 'https' ;
4
4
import * as os from 'os' ;
5
5
import * as path from 'path' ;
6
- import { env , ExtensionContext , ProgressLocation , Uri , window , WorkspaceFolder } from 'vscode' ;
7
- import { downloadFile , executableExists , userAgentHeader } from './utils' ;
6
+ import { promisify } from 'util' ;
7
+ import { env , ExtensionContext , ProgressLocation , Uri , window , workspace , WorkspaceFolder } from 'vscode' ;
8
+ import { downloadFile , executableExists , httpsGetSilently } from './utils' ;
9
+ import * as validate from './validation' ;
8
10
9
11
/** GitHub API release */
10
12
interface IRelease {
11
- assets : [ IAsset ] ;
13
+ assets : IAsset [ ] ;
12
14
tag_name : string ;
13
15
prerelease : boolean ;
14
16
}
@@ -18,6 +20,23 @@ interface IAsset {
18
20
name : string ;
19
21
}
20
22
23
+ type UpdateBehaviour = 'keep-up-to-date' | 'prompt' | 'never-check' ;
24
+
25
+ const assetValidator : validate . Validator < IAsset > = validate . object ( {
26
+ browser_download_url : validate . string ( ) ,
27
+ name : validate . string ( ) ,
28
+ } ) ;
29
+
30
+ const releaseValidator : validate . Validator < IRelease > = validate . object ( {
31
+ assets : validate . array ( assetValidator ) ,
32
+ tag_name : validate . string ( ) ,
33
+ prerelease : validate . boolean ( ) ,
34
+ } ) ;
35
+
36
+ const githubReleaseApiValidator : validate . Validator < IRelease [ ] > = validate . array ( releaseValidator ) ;
37
+
38
+ const cachedReleaseValidator : validate . Validator < IRelease | null > = validate . optional ( releaseValidator ) ;
39
+
21
40
// On Windows the executable needs to be stored somewhere with an .exe extension
22
41
const exeExt = process . platform === 'win32' ? '.exe' : '' ;
23
42
@@ -140,6 +159,75 @@ async function getProjectGhcVersion(context: ExtensionContext, dir: string, rele
140
159
return callWrapper ( downloadedWrapper ) ;
141
160
}
142
161
162
+ async function getLatestReleaseMetadata ( context : ExtensionContext ) : Promise < IRelease | null > {
163
+ const opts : https . RequestOptions = {
164
+ host : 'api.github.com' ,
165
+ path : '/repos/haskell/haskell-language-server/releases' ,
166
+ } ;
167
+
168
+ const offlineCache = path . join ( context . globalStoragePath , 'latestApprovedRelease.cache.json' ) ;
169
+
170
+ async function readCachedReleaseData ( ) : Promise < IRelease | null > {
171
+ try {
172
+ const cachedInfo = await promisify ( fs . readFile ) ( offlineCache , { encoding : 'utf-8' } ) ;
173
+ return validate . parseAndValidate ( cachedInfo , cachedReleaseValidator ) ;
174
+ } catch ( err ) {
175
+ // If file doesn't exist, return null, otherwise consider it a failure
176
+ if ( err . code === 'ENOENT' ) {
177
+ return null ;
178
+ }
179
+ throw err ;
180
+ }
181
+ }
182
+ // Not all users want to upgrade right away, in that case prompt
183
+ const updateBehaviour = workspace . getConfiguration ( 'haskell' ) . get ( 'updateBehavior' ) as UpdateBehaviour ;
184
+
185
+ if ( updateBehaviour === 'never-check' ) {
186
+ return readCachedReleaseData ( ) ;
187
+ }
188
+
189
+ try {
190
+ const releaseInfo = await httpsGetSilently ( opts ) ;
191
+ const latestInfoParsed =
192
+ validate . parseAndValidate ( releaseInfo , githubReleaseApiValidator ) . find ( ( x ) => ! x . prerelease ) || null ;
193
+
194
+ if ( updateBehaviour === 'prompt' ) {
195
+ const cachedInfoParsed = await readCachedReleaseData ( ) ;
196
+
197
+ if (
198
+ latestInfoParsed !== null &&
199
+ ( cachedInfoParsed === null || latestInfoParsed . tag_name !== cachedInfoParsed . tag_name )
200
+ ) {
201
+ const promptMessage =
202
+ cachedInfoParsed === null
203
+ ? 'No version of the haskell-language-server is installed, would you like to install it now?'
204
+ : 'A new version of the haskell-language-server is available, would you like to upgrade now?' ;
205
+
206
+ const decision = await window . showInformationMessage ( promptMessage , 'Download' , 'Nevermind' ) ;
207
+ if ( decision !== 'Download' ) {
208
+ // If not upgrade, bail and don't overwrite cached version information
209
+ return cachedInfoParsed ;
210
+ }
211
+ }
212
+ }
213
+
214
+ // Cache the latest successfully fetched release information
215
+ await promisify ( fs . writeFile ) ( offlineCache , JSON . stringify ( latestInfoParsed ) , { encoding : 'utf-8' } ) ;
216
+ return latestInfoParsed ;
217
+ } catch ( githubError ) {
218
+ // Attempt to read from the latest cached file
219
+ try {
220
+ const cachedInfoParsed = await readCachedReleaseData ( ) ;
221
+
222
+ window . showWarningMessage (
223
+ `Couldn't get the latest haskell-language-server releases from GitHub, used local cache instead:\n${ githubError . message } `
224
+ ) ;
225
+ return cachedInfoParsed ;
226
+ } catch ( fileError ) {
227
+ throw new Error ( `Couldn't get the latest haskell-language-server releases from GitHub:\n${ githubError . message } ` ) ;
228
+ }
229
+ }
230
+ }
143
231
/**
144
232
* Downloads the latest haskell-language-server binaries from GitHub releases.
145
233
* Returns null if it can't find any that match.
@@ -149,27 +237,6 @@ export async function downloadHaskellLanguageServer(
149
237
resource : Uri ,
150
238
folder ?: WorkspaceFolder
151
239
) : Promise < string | null > {
152
- // Fetch the latest release from GitHub
153
- const releases : IRelease [ ] = await new Promise ( ( resolve , reject ) => {
154
- let data : string = '' ;
155
- const opts : https . RequestOptions = {
156
- host : 'api.github.com' ,
157
- path : '/repos/haskell/haskell-language-server/releases' ,
158
- headers : userAgentHeader ,
159
- } ;
160
- https
161
- . get ( opts , ( res ) => {
162
- res . on ( 'data' , ( d ) => ( data += d ) ) ;
163
- res . on ( 'error' , reject ) ;
164
- res . on ( 'close' , ( ) => {
165
- resolve ( JSON . parse ( data ) ) ;
166
- } ) ;
167
- } )
168
- . on ( 'error' , ( e ) => {
169
- reject ( new Error ( `Couldn't get the latest haskell-language-server releases from GitHub:\n${ e . message } ` ) ) ;
170
- } ) ;
171
- } ) ;
172
-
173
240
// Make sure to create this before getProjectGhcVersion
174
241
if ( ! fs . existsSync ( context . globalStoragePath ) ) {
175
242
fs . mkdirSync ( context . globalStoragePath ) ;
@@ -182,13 +249,20 @@ export async function downloadHaskellLanguageServer(
182
249
return null ;
183
250
}
184
251
185
- const release = releases . find ( ( x ) => ! x . prerelease ) ;
252
+ // Fetch the latest release from GitHub or from cache
253
+ const release = await getLatestReleaseMetadata ( context ) ;
186
254
if ( ! release ) {
187
- window . showErrorMessage ( "Couldn't find any pre-built haskell-language-server binaries" ) ;
255
+ let message = "Couldn't find any pre-built haskell-language-server binaries" ;
256
+ const updateBehaviour = workspace . getConfiguration ( 'haskell' ) . get ( 'updateBehavior' ) as UpdateBehaviour ;
257
+ if ( updateBehaviour === 'never-check' ) {
258
+ message += ' (and checking for newer versions is disabled)' ;
259
+ }
260
+ window . showErrorMessage ( message ) ;
188
261
return null ;
189
262
}
190
- const dir : string = folder ?. uri ?. fsPath ?? path . dirname ( resource . fsPath ) ;
191
263
264
+ // Figure out the ghc version to use or advertise an installation link for missing components
265
+ const dir : string = folder ?. uri ?. fsPath ?? path . dirname ( resource . fsPath ) ;
192
266
let ghcVersion : string ;
193
267
try {
194
268
ghcVersion = await getProjectGhcVersion ( context , dir , release ) ;
@@ -224,15 +298,8 @@ export async function downloadHaskellLanguageServer(
224
298
const binaryDest = path . join ( context . globalStoragePath , serverName ) ;
225
299
226
300
const title = `Downloading haskell-language-server ${ release . tag_name } for GHC ${ ghcVersion } ` ;
227
- try {
228
- await downloadFile ( title , asset . browser_download_url , binaryDest ) ;
229
- return binaryDest ;
230
- } catch ( e ) {
231
- if ( e instanceof Error ) {
232
- window . showErrorMessage ( e . message ) ;
233
- }
234
- return null ;
235
- }
301
+ await downloadFile ( title , asset . browser_download_url , binaryDest ) ;
302
+ return binaryDest ;
236
303
}
237
304
238
305
/** Get the OS label used by GitHub for the current platform */
0 commit comments