Skip to content

Commit 52c8d9e

Browse files
committed
Error handling getting ghc version
1 parent ad42765 commit 52c8d9e

File tree

4 files changed

+148
-43
lines changed

4 files changed

+148
-43
lines changed

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,17 @@
103103
"default": "off",
104104
"description": "Traces the communication between VS Code and the language server."
105105
},
106+
"haskell.trace.client": {
107+
"scope": "resource",
108+
"type": "string",
109+
"enum": [
110+
"off",
111+
"error",
112+
"debug"
113+
],
114+
"default": "error",
115+
"description": "Traces the communication between VS Code and the language server."
116+
},
106117
"haskell.logFile": {
107118
"scope": "resource",
108119
"type": "string",

src/extension.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ExecutableOptions,
1616
LanguageClient,
1717
LanguageClientOptions,
18+
Logger,
1819
RevealOutputChannelOn,
1920
ServerOptions,
2021
TransportKind,
@@ -23,7 +24,7 @@ import { CommandNames } from './commands/constants';
2324
import { ImportIdentifier } from './commands/importIdentifier';
2425
import { DocsBrowser } from './docsBrowser';
2526
import { downloadHaskellLanguageServer } from './hlsBinaries';
26-
import { executableExists } from './utils';
27+
import { executableExists, ExtensionLogger } from './utils';
2728

2829
// The current map of documents & folders to language servers.
2930
// It may be null to indicate that we are in the process of launching a server,
@@ -45,7 +46,11 @@ export async function activate(context: ExtensionContext) {
4546
for (const folder of event.removed) {
4647
const client = clients.get(folder.uri.toString());
4748
if (client) {
48-
clients.delete(folder.uri.toString());
49+
const uri = folder.uri.toString();
50+
client.info('Deleting folder for clients: ${uri}');
51+
clients.delete(uri);
52+
client.info;
53+
client.info('Stopping the client');
4954
client.stop();
5055
}
5156
}
@@ -54,10 +59,13 @@ export async function activate(context: ExtensionContext) {
5459
// Register editor commands for HIE, but only register the commands once at activation.
5560
const restartCmd = commands.registerCommand(CommandNames.RestartServerCommandName, async () => {
5661
for (const langClient of clients.values()) {
62+
langClient?.info('Stopping the client');
5763
await langClient?.stop();
64+
langClient?.info('Starting the client');
5865
langClient?.start();
5966
}
6067
});
68+
6169
context.subscriptions.push(restartCmd);
6270

6371
context.subscriptions.push(ImportIdentifier.registerCommand());
@@ -70,30 +78,31 @@ export async function activate(context: ExtensionContext) {
7078
context.subscriptions.push(openOnHackageDisposable);
7179
}
7280

73-
function findManualExecutable(uri: Uri, folder?: WorkspaceFolder): string | null {
81+
function findManualExecutable(logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null {
7482
let exePath = workspace.getConfiguration('haskell', uri).serverExecutablePath;
7583
if (exePath === '') {
7684
return null;
7785
}
78-
86+
logger.info('Trying to find the server executable in: ${exePath}');
7987
// Substitute path variables with their corresponding locations.
8088
exePath = exePath.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir);
8189
if (folder) {
8290
exePath = exePath.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path);
8391
}
84-
92+
logger.info('Location after path variables subsitution: ${exePath}');
8593
if (!executableExists(exePath)) {
8694
throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and is not on the PATH`);
8795
}
8896
return exePath;
8997
}
9098

9199
/** Searches the PATH for whatever is set in serverVariant */
92-
function findLocalServer(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder): string | null {
100+
function findLocalServer(context: ExtensionContext, logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null {
93101
const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server'];
94-
102+
logger.info('Searching for server executables ${exes} in PATH');
95103
for (const exe of exes) {
96104
if (executableExists(exe)) {
105+
logger.info('Found server executable in PATH: ${exe}');
97106
return exe;
98107
}
99108
}
@@ -120,6 +129,9 @@ async function activeServer(context: ExtensionContext, document: TextDocument) {
120129

121130
async function activateServerForFolder(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder) {
122131
const clientsKey = folder ? folder.uri.toString() : uri.toString();
132+
// Set a unique name per workspace folder (useful for multi-root workspaces).
133+
const langName = 'Haskell' + (folder ? ` (${folder.name})` : '');
134+
const outputChannel: OutputChannel = window.createOutputChannel(langName);
123135

124136
// If the client already has an LSP server for this uri/folder, then don't start a new one.
125137
if (clients.has(clientsKey)) {
@@ -129,21 +141,25 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
129141
clients.set(clientsKey, null);
130142

131143
const logLevel = workspace.getConfiguration('haskell', uri).trace.server;
144+
const clientLogLevel = workspace.getConfiguration('haskell', uri).trace.client;
132145
const logFile = workspace.getConfiguration('haskell', uri).logFile;
133146

147+
const logger: Logger = new ExtensionLogger('client', clientLogLevel, outputChannel);
148+
134149
let serverExecutable;
135150
try {
136151
// Try and find local installations first
137-
serverExecutable = findManualExecutable(uri, folder) ?? findLocalServer(context, uri, folder);
152+
serverExecutable = findManualExecutable(logger, uri, folder) ?? findLocalServer(context, logger, uri, folder);
138153
if (serverExecutable === null) {
139154
// If not, then try to download haskell-language-server binaries if it's selected
140-
serverExecutable = await downloadHaskellLanguageServer(context, uri, folder);
155+
serverExecutable = await downloadHaskellLanguageServer(context, logger, uri, folder);
141156
if (!serverExecutable) {
142157
return;
143158
}
144159
}
145160
} catch (e) {
146161
if (e instanceof Error) {
162+
logger.error('Error getting the server executable: ${e.message}');
147163
window.showErrorMessage(e.message);
148164
}
149165
return;
@@ -173,13 +189,9 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
173189
debug: { command: serverExecutable, transport: TransportKind.stdio, args, options: exeOptions },
174190
};
175191

176-
// Set a unique name per workspace folder (useful for multi-root workspaces).
177-
const langName = 'Haskell' + (folder ? ` (${folder.name})` : '');
178-
const outputChannel: OutputChannel = window.createOutputChannel(langName);
179-
outputChannel.appendLine('[client] run command: "' + serverExecutable + ' ' + args.join(' ') + '"');
180-
outputChannel.appendLine('[client] debug command: "' + serverExecutable + ' ' + args.join(' ') + '"');
181-
182-
outputChannel.appendLine(`[client] server cwd: ${exeOptions.cwd}`);
192+
logger.info('run command: "' + serverExecutable + ' ' + args.join(' ') + '"');
193+
logger.info('debug command: "' + serverExecutable + ' ' + args.join(' ') + '"');
194+
logger.info(`server cwd: ${exeOptions.cwd}`);
183195

184196
const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*';
185197
const clientOptions: LanguageClientOptions = {
@@ -213,6 +225,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
213225
langClient.registerProposedFeatures();
214226

215227
// Finally start the client and add it to the list of clients.
228+
logger.info('Starting language client');
216229
langClient.start();
217230
clients.set(clientsKey, langClient);
218231
}

src/hlsBinaries.ts

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as path from 'path';
66
import * as url from 'url';
77
import { promisify } from 'util';
88
import { env, ExtensionContext, ProgressLocation, Uri, window, workspace, WorkspaceFolder } from 'vscode';
9+
import { Logger } from 'vscode-languageclient';
910
import { downloadFile, executableExists, httpsGetSilently } from './utils';
1011
import * as validate from './validation';
1112

@@ -97,33 +98,59 @@ class NoBinariesError extends Error {
9798
* if needed. Returns null if there was an error in either downloading the wrapper or
9899
* in working out the ghc version
99100
*/
100-
async function getProjectGhcVersion(context: ExtensionContext, dir: string, release: IRelease): Promise<string> {
101+
async function getProjectGhcVersion(
102+
context: ExtensionContext,
103+
logger: Logger,
104+
dir: string,
105+
release: IRelease
106+
): Promise<string> {
107+
const title: string = 'Working out the project GHC version. This might take a while...';
108+
logger.info(title);
101109
const callWrapper = (wrapper: string) => {
102110
return window.withProgress(
103111
{
104-
location: ProgressLocation.Window,
105-
title: 'Working out the project GHC version. This might take a while...',
112+
location: ProgressLocation.Notification,
113+
title: title,
114+
cancellable: true,
106115
},
107-
async () => {
116+
async (progress, token) => {
108117
return new Promise<string>((resolve, reject) => {
118+
const command: string = wrapper + ' --project-ghc-version';
119+
logger.info('Executing `${command}` in cwd ${dir} to get the project ghc version');
120+
token.onCancellationRequested(() => {
121+
logger.warn('User canceled the executon of `${command}`');
122+
});
109123
// Need to set the encoding to 'utf8' in order to get back a string
110-
child_process.exec(
111-
wrapper + ' --project-ghc-version',
112-
{ encoding: 'utf8', cwd: dir },
113-
(err, stdout, stderr) => {
114-
if (err) {
115-
const regex = /Cradle requires (.+) but couldn't find it/;
116-
const res = regex.exec(stderr);
117-
if (res) {
118-
throw new MissingToolError(res[1]);
124+
// We execute the command in a shell for windows, to allow use cmd or bat scripts
125+
let childProcess = child_process
126+
.execFile(
127+
command,
128+
{ encoding: 'utf8', cwd: dir, shell: getGithubOS() == 'Windows' },
129+
(err, stdout, stderr) => {
130+
if (err) {
131+
logger.error('Error executing `${command}` with error code ${err.code}');
132+
logger.error('stderr: ${stderr}');
133+
logger.error('stdout: ${stdout}');
134+
const regex = /Cradle requires (.+) but couldn't find it/;
135+
const res = regex.exec(stderr);
136+
if (res) {
137+
throw new MissingToolError(res[1]);
138+
}
139+
throw Error(
140+
`${wrapper} --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}`
141+
);
119142
}
120-
throw Error(
121-
`${wrapper} --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}`
122-
);
143+
resolve(stdout.trim());
123144
}
124-
resolve(stdout.trim());
125-
}
126-
);
145+
)
146+
.on('close', (code, signal) => {
147+
logger.info('Execution of `${command}` closed with code ${err.code} and signal ${signal}');
148+
})
149+
.on('error', (err) => {
150+
logger.error('Error execution `${command}`: name = ${err.name}, message = ${err.message}');
151+
throw err;
152+
});
153+
token.onCancellationRequested((_) => childProcess.kill());
127154
});
128155
}
129156
);
@@ -250,10 +277,12 @@ async function getLatestReleaseMetadata(context: ExtensionContext): Promise<IRel
250277
*/
251278
export async function downloadHaskellLanguageServer(
252279
context: ExtensionContext,
280+
logger: Logger,
253281
resource: Uri,
254282
folder?: WorkspaceFolder
255283
): Promise<string | null> {
256284
// Make sure to create this before getProjectGhcVersion
285+
logger.info('Downloading haskell-language-server');
257286
if (!fs.existsSync(context.globalStoragePath)) {
258287
fs.mkdirSync(context.globalStoragePath);
259288
}
@@ -265,7 +294,7 @@ export async function downloadHaskellLanguageServer(
265294
return null;
266295
}
267296

268-
// Fetch the latest release from GitHub or from cache
297+
logger.info('Fetching the latest release from GitHub or from cache');
269298
const release = await getLatestReleaseMetadata(context);
270299
if (!release) {
271300
let message = "Couldn't find any pre-built haskell-language-server binaries";
@@ -276,12 +305,12 @@ export async function downloadHaskellLanguageServer(
276305
window.showErrorMessage(message);
277306
return null;
278307
}
279-
280-
// Figure out the ghc version to use or advertise an installation link for missing components
308+
logger.info('The latest release is ${release.tag_name}');
309+
logger.info('Figure out the ghc version to use or advertise an installation link for missing components');
281310
const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath);
282311
let ghcVersion: string;
283312
try {
284-
ghcVersion = await getProjectGhcVersion(context, dir, release);
313+
ghcVersion = await getProjectGhcVersion(context, logger, dir, release);
285314
} catch (error) {
286315
if (error instanceof MissingToolError) {
287316
const link = error.installLink();
@@ -304,8 +333,10 @@ export async function downloadHaskellLanguageServer(
304333
// When searching for binaries, use startsWith because the compression may differ
305334
// between .zip and .gz
306335
const assetName = `haskell-language-server-${githubOS}-${ghcVersion}${exeExt}`;
336+
logger.info('Search for binary ${assetName} in release assests');
307337
const asset = release?.assets.find((x) => x.name.startsWith(assetName));
308338
if (!asset) {
339+
logger.error('No binary ${assetName} found in the release assets: ' + release?.assets.map((value) => value.name));
309340
window.showInformationMessage(new NoBinariesError(release.tag_name, ghcVersion).message);
310341
return null;
311342
}
@@ -314,12 +345,14 @@ export async function downloadHaskellLanguageServer(
314345
const binaryDest = path.join(context.globalStoragePath, serverName);
315346

316347
const title = `Downloading haskell-language-server ${release.tag_name} for GHC ${ghcVersion}`;
348+
logger.info(title);
317349
await downloadFile(title, asset.browser_download_url, binaryDest);
318350
if (ghcVersion.startsWith('9.')) {
319-
window.showWarningMessage(
351+
let warning =
320352
'Currently, HLS supports GHC 9 only partially. ' +
321-
'See [issue #297](https://github.com/haskell/haskell-language-server/issues/297) for more detail.'
322-
);
353+
'See [issue #297](https://github.com/haskell/haskell-language-server/issues/297) for more detail.';
354+
logger.warn(warning);
355+
window.showWarningMessage(warning);
323356
}
324357
return binaryDest;
325358
}

src/utils.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,58 @@ import * as https from 'https';
77
import { extname } from 'path';
88
import * as url from 'url';
99
import { promisify } from 'util';
10-
import { ProgressLocation, window } from 'vscode';
10+
import { OutputChannel, ProgressLocation, window } from 'vscode';
11+
import { Logger } from 'vscode-languageclient';
1112
import * as yazul from 'yauzl';
1213
import { createGunzip } from 'zlib';
1314

15+
enum LogLevel {
16+
Off,
17+
Error,
18+
Warn,
19+
Info,
20+
}
21+
export class ExtensionLogger implements Logger {
22+
public readonly name: string;
23+
public readonly level: LogLevel;
24+
public readonly channel: OutputChannel;
25+
26+
constructor(name: string, level: string, channel: OutputChannel) {
27+
this.name = name;
28+
this.level = this.getLogLevel(level);
29+
this.channel = channel;
30+
}
31+
warn(message: string): void {
32+
this.logLevel(LogLevel.Warn, message);
33+
}
34+
info(message: string): void {
35+
this.logLevel(LogLevel.Info, message);
36+
}
37+
38+
error(message: string) {
39+
this.logLevel(LogLevel.Error, message);
40+
}
41+
42+
log(msg: string) {
43+
this.channel.appendLine(msg);
44+
}
45+
46+
private logLevel(level: LogLevel, msg: string) {
47+
if (level <= this.level) this.log('[${name}][${level}] ${msg}');
48+
}
49+
50+
private getLogLevel(level: string) {
51+
switch (level) {
52+
case 'off':
53+
return LogLevel.Off;
54+
case 'error':
55+
return LogLevel.Error;
56+
default:
57+
return LogLevel.Info;
58+
}
59+
}
60+
}
61+
1462
/** When making http requests to github.com, use this header otherwise
1563
* the server will close the request
1664
*/

0 commit comments

Comments
 (0)