Skip to content

Commit 176afc8

Browse files
committed
use local files on connection failure, closes #298
1 parent 35085c4 commit 176afc8

File tree

2 files changed

+61
-34
lines changed

2 files changed

+61
-34
lines changed

src/hlsBinaries.ts

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import * as fs from 'fs';
33
import * as https from 'https';
44
import * as os from 'os';
55
import * as path from 'path';
6+
import { promisify } from 'util';
67
import { env, ExtensionContext, ProgressLocation, Uri, window, WorkspaceFolder } from 'vscode';
7-
import { downloadFile, executableExists, userAgentHeader } from './utils';
8+
import { downloadFile, executableExists, httpsGetSilently } from './utils';
89

910
/** GitHub API release */
1011
interface IRelease {
@@ -140,6 +141,35 @@ async function getProjectGhcVersion(context: ExtensionContext, dir: string, rele
140141
return callWrapper(downloadedWrapper);
141142
}
142143

144+
async function getLatestReleaseMetadata(context: ExtensionContext): Promise<IRelease | undefined> {
145+
const opts: https.RequestOptions = {
146+
host: 'api.github.com',
147+
path: '/repos/haskell/haskell-language-server/releases',
148+
};
149+
const offlineCache = path.join(context.globalStoragePath, 'latestRelease.cache.json');
150+
151+
try {
152+
const releaseInfo = await httpsGetSilently(opts);
153+
const latestInfoParsed = (JSON.parse(releaseInfo) as IRelease[]).find((x) => !x.prerelease);
154+
155+
// Cache the latest successfully fetched release information
156+
await promisify(fs.writeFile)(offlineCache, JSON.stringify(latestInfoParsed), { encoding: 'utf-8' });
157+
return latestInfoParsed;
158+
} catch (githubError) {
159+
// Attempt to read from the latest cached file
160+
try {
161+
const cachedInfo = await promisify(fs.readFile)(offlineCache, { encoding: 'utf-8' });
162+
163+
const cachedInfoParsed = JSON.parse(cachedInfo);
164+
window.showWarningMessage(
165+
`Couldn't get the latest haskell-language-server releases from GitHub, used local cache instead:\n${githubError.message}`
166+
);
167+
return cachedInfoParsed;
168+
} catch (fileError) {
169+
throw new Error(`Couldn't get the latest haskell-language-server releases from GitHub:\n${githubError.message}`);
170+
}
171+
}
172+
}
143173
/**
144174
* Downloads the latest haskell-language-server binaries from GitHub releases.
145175
* Returns null if it can't find any that match.
@@ -149,27 +179,6 @@ export async function downloadHaskellLanguageServer(
149179
resource: Uri,
150180
folder?: WorkspaceFolder
151181
): 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-
173182
// Make sure to create this before getProjectGhcVersion
174183
if (!fs.existsSync(context.globalStoragePath)) {
175184
fs.mkdirSync(context.globalStoragePath);
@@ -182,13 +191,15 @@ export async function downloadHaskellLanguageServer(
182191
return null;
183192
}
184193

185-
const release = releases.find((x) => !x.prerelease);
194+
// Fetch the latest release from GitHub or from cache
195+
const release = await getLatestReleaseMetadata(context);
186196
if (!release) {
187197
window.showErrorMessage("Couldn't find any pre-built haskell-language-server binaries");
188198
return null;
189199
}
190-
const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath);
191200

201+
// Figure out the ghc version to use or advertise an installation link for missing components
202+
const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath);
192203
let ghcVersion: string;
193204
try {
194205
ghcVersion = await getProjectGhcVersion(context, dir, release);
@@ -224,15 +235,8 @@ export async function downloadHaskellLanguageServer(
224235
const binaryDest = path.join(context.globalStoragePath, serverName);
225236

226237
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-
}
238+
await downloadFile(title, asset.browser_download_url, binaryDest);
239+
return binaryDest;
236240
}
237241

238242
/** Get the OS label used by GitHub for the current platform */

src/utils.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { createGunzip } from 'zlib';
1313
/** When making http requests to github.com, use this header otherwise
1414
* the server will close the request
1515
*/
16-
export const userAgentHeader = { 'User-Agent': 'vscode-haskell' };
16+
const userAgentHeader = { 'User-Agent': 'vscode-haskell' };
1717

1818
/** downloadFile may get called twice on the same src and destination:
1919
* When this happens, we should only download the file once but return two
@@ -27,6 +27,29 @@ export const userAgentHeader = { 'User-Agent': 'vscode-haskell' };
2727
*/
2828
const inFlightDownloads = new Map<string, Map<string, Thenable<void>>>();
2929

30+
export async function httpsGetSilently(options: https.RequestOptions): Promise<string> {
31+
const opts: https.RequestOptions = {
32+
...options,
33+
headers: {
34+
...(options.headers ?? {}),
35+
...userAgentHeader,
36+
},
37+
};
38+
39+
return new Promise((resolve, reject) => {
40+
let data: string = '';
41+
https
42+
.get(opts, (res) => {
43+
res.on('data', (d) => (data += d));
44+
res.on('error', reject);
45+
res.on('close', () => {
46+
resolve(data);
47+
});
48+
})
49+
.on('error', reject);
50+
});
51+
}
52+
3053
export async function downloadFile(titleMsg: string, src: string, dest: string): Promise<void> {
3154
// Check to see if we're already in the process of downloading the same thing
3255
const inFlightDownload = inFlightDownloads.get(src)?.get(dest);

0 commit comments

Comments
 (0)