Skip to content

Commit 4ccae6b

Browse files
committed
Decompress gzipped binaries
1 parent 214d9e3 commit 4ccae6b

File tree

4 files changed

+116
-98
lines changed

4 files changed

+116
-98
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vscode-hie-server",
33
"displayName": "Haskell Language Server",
4-
"description": "Language Server Protocol for Haskell via HIE",
4+
"description": "Haskell language support powered by HLS, ghcide or HIE",
55
"version": "0.0.40",
66
"license": "MIT",
77
"publisher": "alanz",
@@ -221,8 +221,8 @@
221221
},
222222
{
223223
"command": "hie.commands.restartHie",
224-
"title": "Haskell: Restart HIE",
225-
"description": "Restart the Hie LSP server"
224+
"title": "Haskell: Restart Haskell LSP server",
225+
"description": "Restart the Haskell LSP server"
226226
}
227227
],
228228
"keybindings": [
@@ -266,7 +266,7 @@
266266
"@types/lodash-es": "^4.17.3",
267267
"@types/lru-cache": "^4.1.2",
268268
"@types/mocha": "^2.2.46",
269-
"@types/node": "^8.10.59",
269+
"@types/node": "^14.0.3",
270270
"@types/request-promise-native": "^1.0.17",
271271
"husky": "^0.14.3",
272272
"prettier": "^2.0.5",

src/download.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import * as fs from 'fs';
44
import * as http from 'http';
55
import * as https from 'https';
6+
import { extname } from 'path';
67
import * as url from 'url';
78
import { ProgressLocation, window } from 'vscode';
9+
import { createGunzip } from 'zlib';
810

911
/** When making http requests to github.com, use this header otherwise
1012
* the server will close the request
@@ -31,9 +33,17 @@ export async function downloadFile(titleMsg: string, srcUrl: url.UrlWithStringQu
3133
};
3234
getWithRedirects(opts, (res) => {
3335
const totalSize = parseInt(res.headers['content-length'] || '1', 10);
34-
const stream = fs.createWriteStream(dest, { mode: 0o744 });
36+
const fileStream = fs.createWriteStream(dest, { mode: 0o744 });
3537
let curSize = 0;
3638

39+
// Decompress it if it's a gzip
40+
const needsUnzip = res.headers['content-type'] === 'application/gzip' || extname(srcUrl.path ?? '') === '.gz';
41+
if (needsUnzip) {
42+
res.pipe(createGunzip()).pipe(fileStream);
43+
} else {
44+
res.pipe(fileStream);
45+
}
46+
3747
function toMB(bytes: number) {
3848
return bytes / (1024 * 1024);
3949
}
@@ -44,9 +54,7 @@ export async function downloadFile(titleMsg: string, srcUrl: url.UrlWithStringQu
4454
progress.report({ message: msg, increment: (chunk.length / totalSize) * 100 });
4555
});
4656
res.on('error', reject);
47-
res.pipe(stream);
48-
stream.on('close', resolve);
49-
// stream.on('end', () => resolve(binaryDest));
57+
fileStream.on('close', resolve);
5058
}).on('error', reject);
5159
});
5260
return p;

src/extension.ts

Lines changed: 97 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ interface IAsset {
4848
export async function activate(context: ExtensionContext) {
4949
// Register HIE to check every time a text document gets opened, to
5050
// support multi-root workspaces.
51+
5152
workspace.onDidOpenTextDocument(async (document: TextDocument) => await activateHie(context, document));
5253
workspace.textDocuments.forEach(async (document: TextDocument) => await activateHie(context, document));
5354

@@ -63,10 +64,10 @@ export async function activate(context: ExtensionContext) {
6364
});
6465
}
6566

66-
async function getProjectGhcVersion(context: ExtensionContext, release: IRelease): Promise<string | null> {
67+
async function getProjectGhcVersion(context: ExtensionContext, dir: string, release: IRelease): Promise<string | null> {
6768
const callWrapper = (wrapper: string) => {
6869
// Need to set the encoding to 'utf8' in order to get back a string
69-
const out = child_process.spawnSync(wrapper, ['--project-ghc-version'], { encoding: 'utf8' });
70+
const out = child_process.spawnSync(wrapper, ['--project-ghc-version'], { encoding: 'utf8', cwd: dir });
7071
return !out.error ? out.stdout.trim() : null;
7172
};
7273

@@ -92,7 +93,9 @@ async function getProjectGhcVersion(context: ExtensionContext, release: IRelease
9293
window.showErrorMessage(`Couldn't find any haskell-language-server-wrapper binaries for ${process.platform}`);
9394
return null;
9495
}
95-
const wrapperAsset = release.assets.find((x) => x.name === `haskell-language-server-wrapper-${githubOS}`);
96+
97+
const assetName = `haskell-language-server-wrapper-${githubOS}.gz`;
98+
const wrapperAsset = release.assets.find((x) => x.name === assetName);
9699

97100
if (!wrapperAsset) {
98101
return null;
@@ -111,44 +114,51 @@ async function getProjectGhcVersion(context: ExtensionContext, release: IRelease
111114
return callWrapper(downloadedWrapper);
112115
}
113116

114-
/** Searches the PATH for whatever is set in hieVariant as well as whatever's in hieExecutablePath */
115-
function findLocalServer(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder): string | null {
116-
const hieVariant = workspace.getConfiguration('languageServerHaskell', uri).hieVariant;
117+
function findManualExecutable(uri: Uri, folder?: WorkspaceFolder): string | null {
117118
let hieExecutablePath = workspace.getConfiguration('languageServerHaskell', uri).hieExecutablePath;
119+
if (hieExecutablePath === '') {
120+
return null;
121+
}
118122

119123
// Substitute path variables with their corresponding locations.
120-
if (hieExecutablePath !== '') {
124+
hieExecutablePath = hieExecutablePath
125+
.replace('${HOME}', os.homedir)
126+
.replace('${home}', os.homedir)
127+
.replace(/^~/, os.homedir);
128+
if (folder) {
121129
hieExecutablePath = hieExecutablePath
122-
.replace('${HOME}', os.homedir)
123-
.replace('${home}', os.homedir)
124-
.replace(/^~/, os.homedir);
125-
if (folder) {
126-
hieExecutablePath = hieExecutablePath
127-
.replace('${workspaceFolder}', folder.uri.path)
128-
.replace('${workspaceRoot}', folder.uri.path);
129-
}
130-
return hieExecutablePath;
130+
.replace('${workspaceFolder}', folder.uri.path)
131+
.replace('${workspaceRoot}', folder.uri.path);
131132
}
132133

134+
if (!executableExists(hieExecutablePath)) {
135+
throw new Error('Manual executable missing');
136+
}
137+
return hieExecutablePath;
138+
}
139+
140+
/** Searches the PATH for whatever is set in hieVariant */
141+
function findLocalServer(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder): string | null {
142+
const hieVariant = workspace.getConfiguration('languageServerHaskell', uri).hieVariant;
143+
133144
// Set the executable, based on the settings.
134-
let serverExecutable = 'hie'; // should get set below
145+
let exes: string[] = []; // should get set below
135146
switch (hieVariant) {
136147
case 'haskell-ide-engine':
137-
serverExecutable = 'hie-wrapper';
148+
exes = ['hie-wrapper', 'hie'];
138149
break;
139150
case 'haskell-language-server':
140-
serverExecutable = 'haskell-language-server-wrapper';
151+
exes = ['haskell-language-server-wrapper', 'haskell-language-server'];
141152
break;
142153
case 'ghcide':
143-
serverExecutable = 'ghcide';
154+
exes = ['ghcide'];
144155
break;
145156
}
146-
if (hieExecutablePath !== '') {
147-
serverExecutable = hieExecutablePath;
148-
}
149157

150-
if (executableExists(serverExecutable)) {
151-
return serverExecutable;
158+
for (const exe in exes) {
159+
if (executableExists(exe)) {
160+
return exe;
161+
}
152162
}
153163

154164
return null;
@@ -172,8 +182,32 @@ function getGithubOS(): string | null {
172182
return platformToGithubOS(process.platform);
173183
}
174184

175-
async function downloadServer(context: ExtensionContext, releases: IRelease[]): Promise<string | null> {
185+
async function downloadServer(
186+
context: ExtensionContext,
187+
resource: Uri,
188+
folder?: WorkspaceFolder
189+
): Promise<string | null> {
190+
// We only download binaries for haskell-language-server at the moment
191+
if (workspace.getConfiguration('languageServerHaskell', resource).hieVariant !== 'haskell-language-server') {
192+
return null;
193+
}
194+
176195
// fetch the latest release from GitHub
196+
const releases: IRelease[] = await new Promise((resolve, reject) => {
197+
let data: string = '';
198+
const opts: https.RequestOptions = {
199+
host: 'api.github.com',
200+
path: '/repos/bubba/haskell-language-server/releases',
201+
headers: userAgentHeader,
202+
};
203+
https.get(opts, (res) => {
204+
res.on('data', (d) => (data += d));
205+
res.on('error', reject);
206+
res.on('close', () => {
207+
resolve(JSON.parse(data));
208+
});
209+
});
210+
});
177211

178212
const githubOS = getGithubOS();
179213
if (githubOS === null) {
@@ -184,15 +218,17 @@ async function downloadServer(context: ExtensionContext, releases: IRelease[]):
184218

185219
// const release = releases.find(x => !x.prerelease);
186220
const release = releases[0];
221+
const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath);
187222

188-
const ghcVersion = await getProjectGhcVersion(context, release);
223+
const ghcVersion = await getProjectGhcVersion(context, dir, release);
189224
if (!ghcVersion) {
190225
window.showErrorMessage("Couldn't figure out what GHC version the project is using");
191226
// We couldn't figure out the right ghc version to download
192227
return null;
193228
}
194229

195-
const asset = release?.assets.find((x) => x.name.includes(githubOS) && x.name.includes(ghcVersion));
230+
const assetName = `haskell-language-server-${githubOS}-${ghcVersion}.gz`;
231+
const asset = release?.assets.find((x) => x.name === assetName);
196232
if (!release || !asset) {
197233
return null;
198234
}
@@ -239,47 +275,6 @@ async function activateHie(context: ExtensionContext, document: TextDocument) {
239275
if (folder && clients.has(folder.uri.toString())) {
240276
return;
241277
}
242-
243-
// try {
244-
// const hieVariant = workspace.getConfiguration('languageServerHaskell', uri).hieVariant;
245-
// const hieExecutablePath = workspace.getConfiguration('languageServerHaskell', uri).hieExecutablePath;
246-
// // Check if hie is installed.
247-
// let exeName = 'hie';
248-
// switch (hieVariant) {
249-
// case 'haskell-ide-engine':
250-
// break;
251-
// case 'haskell-language-server':
252-
// case 'ghcide':
253-
// exeName = hieVariant;
254-
// break;
255-
// }
256-
// if (!await isHieInstalled(exeName) && hieExecutablePath === '') {
257-
// // TODO: Once haskell-ide-engine is on hackage/stackage, enable an option to install it via cabal/stack.
258-
// let hieProjectUrl = '/haskell/haskell-ide-engine';
259-
// switch (hieVariant) {
260-
// case 'haskell-ide-engine':
261-
// break;
262-
// case 'haskell-language-server':
263-
// hieProjectUrl = '/haskell/haskell-language-server';
264-
// break;
265-
// case 'ghcide':
266-
// hieProjectUrl = '/digital-asset/ghcide';
267-
// break;
268-
// }
269-
// const notInstalledMsg: string =
270-
// exeName + ' executable missing, please make sure it is installed, see https://github.com' + hieProjectUrl + '.';
271-
// const forceStart: string = 'Force Start';
272-
// window.showErrorMessage(notInstalledMsg, forceStart).then(option => {
273-
// if (option === forceStart) {
274-
// activateHieNoCheck(context, uri, folder);
275-
// }
276-
// });
277-
// } else {
278-
// activateHieNoCheck(context, uri, folder);
279-
// }
280-
// } catch (e) {
281-
// console.error(e);
282-
// }
283278
activateHieNoCheck(context, uri, folder);
284279
}
285280

@@ -300,24 +295,20 @@ async function activateHieNoCheck(context: ExtensionContext, uri: Uri, folder?:
300295
const logLevel = workspace.getConfiguration('languageServerHaskell', uri).trace.server;
301296
const logFile = workspace.getConfiguration('languageServerHaskell', uri).logFile;
302297

303-
const releases: IRelease[] = await new Promise((resolve, reject) => {
304-
let data: string = '';
305-
const opts: https.RequestOptions = {
306-
host: 'api.github.com',
307-
path: '/repos/bubba/haskell-language-server/releases',
308-
headers: userAgentHeader,
309-
};
310-
https.get(opts, (res) => {
311-
res.on('data', (d) => (data += d));
312-
res.on('error', reject);
313-
res.on('close', () => {
314-
resolve(JSON.parse(data));
315-
});
316-
});
317-
});
318-
319-
const serverExecutable = findLocalServer(context, uri, folder) ?? (await downloadServer(context, releases));
320-
if (serverExecutable === null) {
298+
let serverExecutable;
299+
try {
300+
serverExecutable =
301+
findManualExecutable(uri, folder) ??
302+
findLocalServer(context, uri, folder) ??
303+
(await downloadServer(context, uri, folder));
304+
if (serverExecutable === null) {
305+
showNotInstalledErrorMessage(uri);
306+
return;
307+
}
308+
} catch (e) {
309+
if (e instanceof Error) {
310+
window.showErrorMessage(e.message);
311+
}
321312
return;
322313
}
323314

@@ -467,3 +458,22 @@ async function registerHiePointCommand(name: string, command: string, context: E
467458
});
468459
context.subscriptions.push(editorCmd);
469460
}
461+
462+
function showNotInstalledErrorMessage(uri: Uri) {
463+
const variant = workspace.getConfiguration('languageServerHaskell', uri).hieVariant;
464+
let projectUrl = '';
465+
switch (variant) {
466+
case 'haskell-ide-engine':
467+
projectUrl = '/haskell/haskell-ide-engine';
468+
break;
469+
case 'haskell-language-server':
470+
projectUrl = '/haskell/haskell-language-server';
471+
break;
472+
case 'ghcide':
473+
projectUrl = '/digital-asset/ghcide';
474+
break;
475+
}
476+
const notInstalledMsg: string =
477+
variant + ' executable missing, please make sure it is installed, see https://github.com' + projectUrl + '.';
478+
window.showErrorMessage(notInstalledMsg);
479+
}

0 commit comments

Comments
 (0)