Skip to content

Commit

Permalink
Ignore missing entries in Release Metadata
Browse files Browse the repository at this point in the history
Add various logging statements.
Demote excessive logging to debug level.
Refactor hard to read functions.
Add more typing information.
  • Loading branch information
fendor committed Apr 17, 2022
1 parent f6e701e commit 74cc97b
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 94 deletions.
10 changes: 5 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
// only list environment variables that we actually care about.
// this makes it safe for users to just paste the logs to whoever,
// and avoids leaking secrets.
if (["PATH"].includes(key)) {
if (['PATH'].includes(key)) {
logger.log(` ${key}: ${value}`);
}
});
Expand Down Expand Up @@ -175,16 +175,16 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
}
} else if (e instanceof HlsError) {
logger.error(`General HlsError: ${e.message}`);
if (e.stack) {
logger.error(`${e.stack}`);
}
window.showErrorMessage(e.message);
} else if (e instanceof Error) {
logger.error(`Internal Error: ${e.message}`);
window.showErrorMessage(e.message);
}
if (e instanceof Error) {
// general stack trace printing
if (e.stack) {
logger.error(`${e.stack}`);
}
window.showErrorMessage(e.message);
}
return;
}
Expand Down
202 changes: 113 additions & 89 deletions src/hlsBinaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Logger } from 'vscode-languageclient';
import { HlsError, MissingToolError, NoMatchingHls } from './errors';
import {
addPathToProcessPath,
comparePVP,
executableExists,
httpsGetSilently,
IEnvVars,
Expand All @@ -24,14 +25,23 @@ type Tool = 'hls' | 'ghc' | 'cabal' | 'stack';

type ToolConfig = Map<Tool, string>;

export type ReleaseMetadata = Map<string, Map<string, Map<string, string[]>>>;

type ManageHLS = 'GHCup' | 'PATH';
let manageHLS = workspace.getConfiguration('haskell').get('manageHLS') as ManageHLS;

// On Windows the executable needs to be stored somewhere with an .exe extension
const exeExt = process.platform === 'win32' ? '.exe' : '';

/**
* Callback invoked on process termination.
*/
type ProcessCallback = (
error: ExecException | null,
stdout: string,
stderr: string,
resolve: (value: string | PromiseLike<string>) => void,
reject: (reason?: any) => void
) => void;

/**
* Call a process asynchronously.
* While doing so, update the windows with progress information.
Expand All @@ -45,7 +55,7 @@ const exeExt = process.platform === 'win32' ? '.exe' : '';
* @param title Title of the action, shown to users if available.
* @param cancellable Can the user cancel this process invocation?
* @param envAdd Extra environment variables for this process only.
* @param callback Upon process termination, execute this callback. If given, must resolve promise.
* @param callback Upon process termination, execute this callback. If given, must resolve promise. On error, stderr and stdout are logged regardless of whether the callback has been specified.
* @returns Stdout of the process invocation, trimmed off newlines, or whatever the `callback` resolved to.
*/
async function callAsync(
Expand All @@ -56,13 +66,7 @@ async function callAsync(
title?: string,
cancellable?: boolean,
envAdd?: IEnvVars,
callback?: (
error: ExecException | null,
stdout: string,
stderr: string,
resolve: (value: string | PromiseLike<string>) => void,
reject: (reason?: any) => void
) => void
callback?: ProcessCallback
): Promise<string> {
let newEnv: IEnvVars = await resolveServerEnvironmentPATH(
workspace.getConfiguration('haskell').get('serverEnvironment') || {}
Expand All @@ -89,15 +93,17 @@ async function callAsync(
args,
{ encoding: 'utf8', cwd: dir, shell: process.platform === 'win32', env: newEnv },
(err, stdout, stderr) => {
if (err) {
logger.error(`Error executing '${command}' with error code ${err.code}`);
logger.error(`stderr: ${stderr}`);
if (stdout) {
logger.error(`stdout: ${stdout}`);
}
}
if (callback) {
callback(err, stdout, stderr, resolve, reject);
} else {
if (err) {
logger.error(`Error executing '${command}' with error code ${err.code}`);
logger.error(`stderr: ${stderr}`);
if (stdout) {
logger.error(`stdout: ${stdout}`);
}
reject(
Error(`\`${command}\` exited with exit code ${err.code}.
Consult the [Extensions Output](https://github.com/haskell/vscode-haskell#investigating-and-reporting-problems)
Expand All @@ -112,7 +118,7 @@ async function callAsync(
.on('exit', (code, signal) => {
const msg =
`Execution of '${command}' terminated with code ${code}` + (signal ? `and signal ${signal}` : '');
logger.info(msg);
logger.log(msg);
})
.on('error', (err) => {
if (err) {
Expand Down Expand Up @@ -292,7 +298,9 @@ export async function findHaskellLanguageServer(
"Yes, don't ask again"
);
if (decision === 'Yes') {
logger.info(`User accepted download for ${toInstall.join(', ')}.`);
} else if (decision === "Yes, don't ask again") {
logger.info(`User accepted download for ${toInstall.join(', ')} and won't be asked again.`);
workspace.getConfiguration('haskell').update('promptBeforeDownloads', false);
} else {
[hlsInstalled, cabalInstalled, stackInstalled, ghcInstalled].forEach((tool) => {
Expand Down Expand Up @@ -363,7 +371,9 @@ export async function findHaskellLanguageServer(
"Yes, don't ask again"
);
if (decision === 'Yes') {
logger.info(`User accepted download for ${toInstall.join(', ')}.`);
} else if (decision === "Yes, don't ask again") {
logger.info(`User accepted download for ${toInstall.join(', ')} and won't be asked again.`);
workspace.getConfiguration('haskell').update('promptBeforeDownloads', false);
} else {
[hlsInstalled, ghcInstalled].forEach((tool) => {
Expand Down Expand Up @@ -410,13 +420,7 @@ async function callGHCup(
args: string[],
title?: string,
cancellable?: boolean,
callback?: (
error: ExecException | null,
stdout: string,
stderr: string,
resolve: (value: string | PromiseLike<string>) => void,
reject: (reason?: any) => void
) => void
callback?: ProcessCallback
): Promise<string> {
const metadataUrl = workspace.getConfiguration('haskell').metadataURL;

Expand Down Expand Up @@ -510,13 +514,7 @@ export async function getProjectGHCVersion(
false,
environmentNew,
(err, stdout, stderr, resolve, reject) => {
const command: string = 'haskell-language-server-wrapper' + ' ' + args.join(' ');
if (err) {
logger.error(`Error executing '${command}' with error code ${err.code}`);
logger.error(`stderr: ${stderr}`);
if (stdout) {
logger.error(`stdout: ${stdout}`);
}
// Error message emitted by HLS-wrapper
const regex =
/Cradle requires (.+) but couldn't find it|The program \'(.+)\' version .* is required but the version of.*could.*not be determined|Cannot find the program \'(.+)\'\. User-specified/;
Expand Down Expand Up @@ -576,43 +574,6 @@ export async function findGHCup(context: ExtensionContext, logger: Logger, folde
}
}

/**
* Compare the PVP versions of two strings.
* Details: https://github.com/haskell/pvp/
*
* @param l First version
* @param r second version
* @returns `1` if l is newer than r, `0` if they are equal and `-1` otherwise.
*/
export function comparePVP(l: string, r: string): number {
const al = l.split('.');
const ar = r.split('.');

let eq = 0;

for (let i = 0; i < Math.max(al.length, ar.length); i++) {
const el = parseInt(al[i], 10) || undefined;
const er = parseInt(ar[i], 10) || undefined;

if (el === undefined && er === undefined) {
break;
} else if (el !== undefined && er === undefined) {
eq = 1;
break;
} else if (el === undefined && er !== undefined) {
eq = -1;
break;
} else if (el !== undefined && er !== undefined && el > er) {
eq = 1;
break;
} else if (el !== undefined && er !== undefined && el < er) {
eq = -1;
break;
}
}
return eq;
}

export async function getStoragePath(context: ExtensionContext): Promise<string> {
let storagePath: string | undefined = await workspace.getConfiguration('haskell').get('releasesDownloadStoragePath');

Expand Down Expand Up @@ -677,7 +638,7 @@ async function getLatestAvailableToolFromGHCup(
}
}

// complements getLatestHLSfromMetadata, by checking possibly locally compiled
// complements getHLSesfromMetadata, by checking possibly locally compiled
// HLS in ghcup
// If 'targetGhc' is omitted, picks the latest 'haskell-language-server-wrapper',
// otherwise ensures the specified GHC is supported.
Expand Down Expand Up @@ -730,14 +691,50 @@ async function toolInstalled(
}

/**
* Given a GHC version, download at least one HLS version that can be used.
* This also honours the OS architecture we are on.
* Metadata of release information.
*
* Example of the expected format:
*
* ```
* {
* "1.6.1.0": {
* "A_64": {
* "Darwin": [
* "8.10.6",
* ],
* "Linux_Alpine": [
* "8.10.7",
* "8.8.4",
* ],
* },
* "A_ARM": {
* "Linux_UnknownLinux": [
* "8.10.7"
* ]
* },
* "A_ARM64": {
* "Darwin": [
* "8.10.7"
* ],
* "Linux_UnknownLinux": [
* "8.10.7"
* ]
* }
* }
* }
* ```
*
* consult [ghcup metadata repo](https://github.com/haskell/ghcup-metadata/) for details.
*/
export type ReleaseMetadata = Map<string, Map<string, Map<string, string[]>>>;

/**
* Compute Map of supported HLS versions for this platform.
* Fetches HLS metadata information.
*
* @param context Context of the extension, required for metadata.
* @param storagePath Path to store binaries, caching information, etc...
* @param targetGhc GHC version we want a HLS for.
* @param logger Logger for feedback
* @returns
* @returns Map of supported HLS versions or null if metadata could not be fetched.
*/
async function getHLSesfromMetadata(context: ExtensionContext, logger: Logger): Promise<Map<string, string[]> | null> {
const storagePath: string = await getStoragePath(context);
Expand All @@ -746,32 +743,59 @@ async function getHLSesfromMetadata(context: ExtensionContext, logger: Logger):
window.showErrorMessage('Could not get release metadata');
return null;
}
const plat = match(process.platform)
.with('darwin', (_) => 'Darwin')
.with('linux', (_) => 'Linux_UnknownLinux')
.with('win32', (_) => 'Windows')
.with('freebsd', (_) => 'FreeBSD')
const plat: Platform | null = match(process.platform)
.with('darwin', (_) => 'Darwin' as Platform)
.with('linux', (_) => 'Linux_UnknownLinux' as Platform)
.with('win32', (_) => 'Windows' as Platform)
.with('freebsd', (_) => 'FreeBSD' as Platform)
.otherwise((_) => null);
if (plat === null) {
throw new Error(`Unknown platform ${process.platform}`);
}
const arch = match(process.arch)
.with('arm', (_) => 'A_ARM')
.with('arm64', (_) => 'A_ARM64')
.with('x32', (_) => 'A_32')
.with('x64', (_) => 'A_64')
const arch: Arch | null = match(process.arch)
.with('arm', (_) => 'A_ARM' as Arch)
.with('arm64', (_) => 'A_ARM64' as Arch)
.with('x32', (_) => 'A_32' as Arch)
.with('x64', (_) => 'A_64' as Arch)
.otherwise((_) => null);
if (arch === null) {
throw new Error(`Unknown architecture ${process.arch}`);
}
logger.info(`Platform constants: ${plat}, ${arch}`);

return findSupportedHlsPerGhc(plat, arch, metadata, logger);
}

const map: ReleaseMetadata = new Map(Object.entries(metadata));
export type Platform = 'Darwin' | 'Linux_UnknownLinux' | 'Windows' | 'FreeBSD';

export type Arch = 'A_ARM' | 'A_ARM64' | 'A_32' | 'A_64';

/**
* Find all supported GHC versions per HLS version supported on the given
* platform and architecture.
* @param platform Platform of the host.
* @param arch Arch of the host.
* @param metadata HLS Metadata information.
* @param logger Logger.
* @returns Map from HLS version to GHC versions that are supported.
*/
export function findSupportedHlsPerGhc(
platform: Platform,
arch: Arch,
metadata: ReleaseMetadata,
logger: Logger
): Map<string, string[]> {
const newMap = new Map<string, string[]>();
map.forEach((value, key) => {
const value_ = new Map(Object.entries(value));
const archValues = new Map(Object.entries(value_.get(arch)));
const versions: string[] = archValues.get(plat) as string[];
newMap.set(key, versions);
metadata.forEach((supportedArch, hlsVersion) => {
logger.log(`HLS Version: ${hlsVersion}`);
const supportedOs = supportedArch.get(arch);
if (supportedOs) {
const ghcSupportedOnOs = supportedOs.get(platform);
if (ghcSupportedOnOs) {
logger.log(` ${hlsVersion} compatible with GHC Versions: ${ghcSupportedOnOs}`);
newMap.set(hlsVersion, ghcSupportedOnOs);
}
}
});

return newMap;
Expand Down Expand Up @@ -850,7 +874,7 @@ async function getReleaseMetadata(
*/
class InstalledTool {
/**
* "<name>-<version>" of the installed Tool.
* "\<name\>-\<version\>" of the installed Tool.
*/
readonly nameWithVersion: string = '';

Expand Down
Loading

0 comments on commit 74cc97b

Please sign in to comment.