Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8f035a4

Browse files
authoredAug 4, 2021
Merge pull request #422 from jneira/fix-ghc-version
- Add much more logging in the client side, configured with haskell.trace.client - Fix error handling of working out project ghc (See #421) - And dont use a shell to spawn the subprocess in non windows systems - Add commands Start Haskell LSP server and Stop Haskell LSP server
2 parents aafd659 + 61d8e3f commit 8f035a4

File tree

7 files changed

+216
-58
lines changed

7 files changed

+216
-58
lines changed
 

‎Changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
- Add tracking of cabal files to work together with the incoming cabal formatter plugin
44

5+
### 1.5.1
6+
7+
- Add much more logging in the client side, configured with `haskell.trace.client`
8+
- Fix error handling of `working out project ghc` (See #421)
9+
- And dont use a shell to spawn the subprocess in non windows systems
10+
- Show the progress as a cancellable notification
11+
- Add commands `Start Haskell LSP server` and `Stop Haskell LSP server`
12+
513
### 1.5.0
614

715
- Emit warning about limited support for ghc-9.x on hls executable download

‎package-lock.json

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

‎package.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "haskell",
33
"displayName": "Haskell",
44
"description": "Haskell language support powered by the Haskell Language Server",
5-
"version": "1.5.0",
5+
"version": "1.5.1",
66
"license": "MIT",
77
"publisher": "haskell",
88
"engines": {
@@ -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",
@@ -312,6 +323,16 @@
312323
"command": "haskell.commands.restartServer",
313324
"title": "Haskell: Restart Haskell LSP server",
314325
"description": "Restart the Haskell LSP server"
326+
},
327+
{
328+
"command": "haskell.commands.startServer",
329+
"title": "Haskell: Start Haskell LSP server",
330+
"description": "Start the Haskell LSP server"
331+
},
332+
{
333+
"command": "haskell.commands.stopServer",
334+
"title": "Haskell: Stop Haskell LSP server",
335+
"description": "Stop the Haskell LSP server"
315336
}
316337
]
317338
},

‎src/commands/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export namespace CommandNames {
22
export const ImportIdentifierCommandName = 'haskell.commands.importIdentifier';
33
export const RestartServerCommandName = 'haskell.commands.restartServer';
4+
export const StartServerCommandName = 'haskell.commands.startServer';
5+
export const StopServerCommandName = 'haskell.commands.stopServer';
46
}

‎src/extension.ts

Lines changed: 58 additions & 17 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,10 @@ 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('Stopping the server');
4953
client.stop();
5054
}
5155
}
@@ -54,12 +58,35 @@ export async function activate(context: ExtensionContext) {
5458
// Register editor commands for HIE, but only register the commands once at activation.
5559
const restartCmd = commands.registerCommand(CommandNames.RestartServerCommandName, async () => {
5660
for (const langClient of clients.values()) {
61+
langClient?.info('Stopping the server');
5762
await langClient?.stop();
63+
langClient?.info('Starting the server');
5864
langClient?.start();
5965
}
6066
});
67+
6168
context.subscriptions.push(restartCmd);
6269

70+
const stopCmd = commands.registerCommand(CommandNames.StopServerCommandName, async () => {
71+
for (const langClient of clients.values()) {
72+
langClient?.info('Stopping the server');
73+
await langClient?.stop();
74+
langClient?.info('Server stopped');
75+
}
76+
});
77+
78+
context.subscriptions.push(stopCmd);
79+
80+
const startCmd = commands.registerCommand(CommandNames.StartServerCommandName, async () => {
81+
for (const langClient of clients.values()) {
82+
langClient?.info('Starting the server');
83+
langClient?.start();
84+
langClient?.info('Server started');
85+
}
86+
});
87+
88+
context.subscriptions.push(startCmd);
89+
6390
context.subscriptions.push(ImportIdentifier.registerCommand());
6491

6592
// Set up the documentation browser.
@@ -70,30 +97,31 @@ export async function activate(context: ExtensionContext) {
7097
context.subscriptions.push(openOnHackageDisposable);
7198
}
7299

73-
function findManualExecutable(uri: Uri, folder?: WorkspaceFolder): string | null {
100+
function findManualExecutable(logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null {
74101
let exePath = workspace.getConfiguration('haskell', uri).serverExecutablePath;
75102
if (exePath === '') {
76103
return null;
77104
}
78-
105+
logger.info(`Trying to find the server executable in: ${exePath}`);
79106
// Substitute path variables with their corresponding locations.
80107
exePath = exePath.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir);
81108
if (folder) {
82109
exePath = exePath.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path);
83110
}
84-
111+
logger.info(`Location after path variables subsitution: ${exePath}`);
85112
if (!executableExists(exePath)) {
86-
throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and is not on the PATH`);
113+
throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and it is not on the PATH`);
87114
}
88115
return exePath;
89116
}
90117

91118
/** Searches the PATH for whatever is set in serverVariant */
92-
function findLocalServer(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder): string | null {
119+
function findLocalServer(context: ExtensionContext, logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null {
93120
const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server'];
94-
121+
logger.info(`Searching for server executables ${exes.join(',')} in $PATH`);
95122
for (const exe of exes) {
96123
if (executableExists(exe)) {
124+
logger.info(`Found server executable in $PATH: ${exe}`);
97125
return exe;
98126
}
99127
}
@@ -120,6 +148,9 @@ async function activeServer(context: ExtensionContext, document: TextDocument) {
120148

121149
async function activateServerForFolder(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder) {
122150
const clientsKey = folder ? folder.uri.toString() : uri.toString();
151+
// Set a unique name per workspace folder (useful for multi-root workspaces).
152+
const langName = 'Haskell' + (folder ? ` (${folder.name})` : '');
153+
const outputChannel: OutputChannel = window.createOutputChannel(langName);
123154

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

131162
const logLevel = workspace.getConfiguration('haskell', uri).trace.server;
163+
const clientLogLevel = workspace.getConfiguration('haskell', uri).trace.client;
132164
const logFile = workspace.getConfiguration('haskell', uri).logFile;
133165

166+
const logger: Logger = new ExtensionLogger('client', clientLogLevel, outputChannel);
167+
134168
let serverExecutable;
135169
try {
136170
// Try and find local installations first
137-
serverExecutable = findManualExecutable(uri, folder) ?? findLocalServer(context, uri, folder);
171+
serverExecutable = findManualExecutable(logger, uri, folder) ?? findLocalServer(context, logger, uri, folder);
138172
if (serverExecutable === null) {
139173
// If not, then try to download haskell-language-server binaries if it's selected
140-
serverExecutable = await downloadHaskellLanguageServer(context, uri, folder);
174+
serverExecutable = await downloadHaskellLanguageServer(context, logger, uri, folder);
141175
if (!serverExecutable) {
142176
return;
143177
}
144178
}
145179
} catch (e) {
146180
if (e instanceof Error) {
181+
logger.error(`Error getting the server executable: ${e.message}`);
147182
window.showErrorMessage(e.message);
148183
}
149184
return;
@@ -162,6 +197,12 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
162197
// If we're operating on a standalone file (i.e. not in a folder) then we need
163198
// to launch the server in a reasonable current directory. Otherwise the cradle
164199
// guessing logic in hie-bios will be wrong!
200+
if (folder) {
201+
logger.info(`Activating the language server in the workspace folder: ${folder?.uri.fsPath}`);
202+
} else {
203+
logger.info(`Activating the language server in the parent dir of the file: ${uri.fsPath}`);
204+
}
205+
165206
const exeOptions: ExecutableOptions = {
166207
cwd: folder ? undefined : path.dirname(uri.fsPath),
167208
};
@@ -173,15 +214,14 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
173214
debug: { command: serverExecutable, transport: TransportKind.stdio, args, options: exeOptions },
174215
};
175216

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}`);
217+
logger.info(`run command: ${serverExecutable} ${args.join(' ')}`);
218+
logger.info(`debug command: ${serverExecutable} ${args.join(' ')}`);
219+
if (exeOptions.cwd) {
220+
logger.info(`server cwd: ${exeOptions.cwd}`);
221+
}
183222

184223
const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*';
224+
logger.info(`document selector patten: ${pat}`);
185225
const clientOptions: LanguageClientOptions = {
186226
// Use the document selector to only notify the LSP on files inside the folder
187227
// path for the specific workspace.
@@ -213,6 +253,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
213253
langClient.registerProposedFeatures();
214254

215255
// Finally start the client and add it to the list of clients.
256+
logger.info('Starting language server');
216257
langClient.start();
217258
clients.set(clientsKey, langClient);
218259
}

‎src/hlsBinaries.ts

Lines changed: 71 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,69 @@ 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 args = ['--project-ghc-version'];
119+
const command: string = wrapper + args.join(' ');
120+
logger.info(`Executing '${command}' in cwd '${dir}' to get the project or file ghc version`);
121+
token.onCancellationRequested(() => {
122+
logger.warn(`User canceled the execution of '${command}'`);
123+
});
109124
// 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]);
125+
// We execute the command in a shell for windows, to allow use .cmd or .bat scripts
126+
const childProcess = child_process
127+
.execFile(
128+
wrapper,
129+
args,
130+
{ encoding: 'utf8', cwd: dir, shell: getGithubOS() === 'Windows' },
131+
(err, stdout, stderr) => {
132+
if (err) {
133+
logger.error(`Error executing '${command}' with error code ${err.code}`);
134+
logger.error(`stderr: ${stderr}`);
135+
if (stdout) {
136+
logger.error(`stdout: ${stdout}`);
137+
}
138+
const regex = /Cradle requires (.+) but couldn't find it/;
139+
const res = regex.exec(stderr);
140+
if (res) {
141+
reject(new MissingToolError(res[1]));
142+
}
143+
reject(
144+
Error(`${wrapper} --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}`)
145+
);
146+
} else {
147+
logger.info(`The GHC version for the project or file: ${stdout?.trim()}`);
148+
resolve(stdout?.trim());
119149
}
120-
throw Error(
121-
`${wrapper} --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}`
122-
);
123150
}
124-
resolve(stdout.trim());
125-
}
126-
);
151+
)
152+
.on('exit', (code, signal) => {
153+
const msg =
154+
`Execution of '${command}' terminated with code ${code}` + (signal ? `and signal ${signal}` : '');
155+
logger.info(msg);
156+
})
157+
.on('error', (err) => {
158+
if (err) {
159+
logger.error(`Error executing '${command}': name = ${err.name}, message = ${err.message}`);
160+
reject(err);
161+
}
162+
});
163+
token.onCancellationRequested((_) => childProcess.kill());
127164
});
128165
}
129166
);
@@ -250,10 +287,12 @@ async function getLatestReleaseMetadata(context: ExtensionContext): Promise<IRel
250287
*/
251288
export async function downloadHaskellLanguageServer(
252289
context: ExtensionContext,
290+
logger: Logger,
253291
resource: Uri,
254292
folder?: WorkspaceFolder
255293
): Promise<string | null> {
256294
// Make sure to create this before getProjectGhcVersion
295+
logger.info('Downloading haskell-language-server');
257296
if (!fs.existsSync(context.globalStoragePath)) {
258297
fs.mkdirSync(context.globalStoragePath);
259298
}
@@ -265,7 +304,7 @@ export async function downloadHaskellLanguageServer(
265304
return null;
266305
}
267306

268-
// Fetch the latest release from GitHub or from cache
307+
logger.info('Fetching the latest release from GitHub or from cache');
269308
const release = await getLatestReleaseMetadata(context);
270309
if (!release) {
271310
let message = "Couldn't find any pre-built haskell-language-server binaries";
@@ -276,12 +315,12 @@ export async function downloadHaskellLanguageServer(
276315
window.showErrorMessage(message);
277316
return null;
278317
}
279-
280-
// Figure out the ghc version to use or advertise an installation link for missing components
318+
logger.info(`The latest release is ${release.tag_name}`);
319+
logger.info('Figure out the ghc version to use or advertise an installation link for missing components');
281320
const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath);
282321
let ghcVersion: string;
283322
try {
284-
ghcVersion = await getProjectGhcVersion(context, dir, release);
323+
ghcVersion = await getProjectGhcVersion(context, logger, dir, release);
285324
} catch (error) {
286325
if (error instanceof MissingToolError) {
287326
const link = error.installLink();
@@ -304,8 +343,12 @@ export async function downloadHaskellLanguageServer(
304343
// When searching for binaries, use startsWith because the compression may differ
305344
// between .zip and .gz
306345
const assetName = `haskell-language-server-${githubOS}-${ghcVersion}${exeExt}`;
346+
logger.info(`Search for binary ${assetName} in release assests`);
307347
const asset = release?.assets.find((x) => x.name.startsWith(assetName));
308348
if (!asset) {
349+
logger.error(
350+
`No binary ${assetName} found in the release assets: ${release?.assets.map((value) => value.name).join(',')}`
351+
);
309352
window.showInformationMessage(new NoBinariesError(release.tag_name, ghcVersion).message);
310353
return null;
311354
}
@@ -314,12 +357,14 @@ export async function downloadHaskellLanguageServer(
314357
const binaryDest = path.join(context.globalStoragePath, serverName);
315358

316359
const title = `Downloading haskell-language-server ${release.tag_name} for GHC ${ghcVersion}`;
360+
logger.info(title);
317361
await downloadFile(title, asset.browser_download_url, binaryDest);
318362
if (ghcVersion.startsWith('9.')) {
319-
window.showWarningMessage(
363+
const warning =
320364
'Currently, HLS supports GHC 9 only partially. ' +
321-
'See [issue #297](https://github.com/haskell/haskell-language-server/issues/297) for more detail.'
322-
);
365+
'See [issue #297](https://github.com/haskell/haskell-language-server/issues/297) for more detail.';
366+
logger.warn(warning);
367+
window.showWarningMessage(warning);
323368
}
324369
return binaryDest;
325370
}

‎src/utils.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,61 @@ 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+
public warn(message: string): void {
32+
this.logLevel(LogLevel.Warn, message);
33+
}
34+
35+
public info(message: string): void {
36+
this.logLevel(LogLevel.Info, message);
37+
}
38+
39+
public error(message: string) {
40+
this.logLevel(LogLevel.Error, message);
41+
}
42+
43+
public log(msg: string) {
44+
this.channel.appendLine(msg);
45+
}
46+
47+
private logLevel(level: LogLevel, msg: string) {
48+
if (level <= this.level) {
49+
this.log(`[${this.name}][${LogLevel[level].toUpperCase()}] ${msg}`);
50+
}
51+
}
52+
53+
private getLogLevel(level: string) {
54+
switch (level) {
55+
case 'off':
56+
return LogLevel.Off;
57+
case 'error':
58+
return LogLevel.Error;
59+
default:
60+
return LogLevel.Info;
61+
}
62+
}
63+
}
64+
1465
/** When making http requests to github.com, use this header otherwise
1566
* the server will close the request
1667
*/

0 commit comments

Comments
 (0)
Please sign in to comment.