src: add binary path to goVersion

Promisify the getGoVersion function and add the binary path to the goVersion type. This will simplify things because the two are often used together. There is probably much more clean up that can be done here, but I'm trying to make small CLs :)

Change-Id: I853de52f95ec14cb498962810eae5c5efd1ff878
GitHub-Last-Rev: fbed067ef305b88dadf26627315a5de2999b043e
GitHub-Pull-Request: golang/vscode-go#75
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/234533
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts
index 1d2bc4e..eb641af 100644
--- a/src/goInstallTools.ts
+++ b/src/goInstallTools.ts
@@ -93,13 +93,6 @@
  * @param goVersion version of Go that affects how to install the tool. (e.g. modules vs legacy GOPATH mode)
  */
 export async function installTools(missing: ToolAtVersion[], goVersion: GoVersion): Promise<void> {
-	const goRuntimePath = getBinPath('go');
-	if (!goRuntimePath) {
-		vscode.window.showErrorMessage(
-			`Failed to run "go get" to install the packages as the "go" binary cannot be found in either GOROOT(${process.env['GOROOT']}) or PATH(${envPath})`
-		);
-		return;
-	}
 	if (!missing) {
 		return;
 	}
@@ -148,7 +141,7 @@
 		// Disable modules for tools which are installed with the "..." wildcard.
 		const modulesOffForTool = modulesOff || disableModulesForWildcard(tool, goVersion);
 
-		const reason = installTool(tool, goRuntimePath, goVersion, envForTools, !modulesOffForTool);
+		const reason = installTool(tool, goVersion, envForTools, !modulesOffForTool);
 		toInstall.push(Promise.resolve({ tool, reason: await reason }));
 	}
 
@@ -179,7 +172,7 @@
 }
 
 export async function installTool(
-	tool: ToolAtVersion, goRuntimePath: string, goVersion: GoVersion,
+	tool: ToolAtVersion, goVersion: GoVersion,
 	envForTools: NodeJS.Dict<string>, modulesOn: boolean): Promise<string> {
 	// Some tools may have to be closed before we reinstall them.
 	if (tool.close) {
@@ -231,19 +224,19 @@
 			cwd: toolsTmpDir,
 		};
 		const execFile = util.promisify(cp.execFile);
-		const { stdout, stderr } = await execFile(goRuntimePath, args, opts);
+		const { stdout, stderr } = await execFile(goVersion.binaryPath, args, opts);
 		output = `${stdout} ${stderr}`;
 
 		// TODO(rstambler): Figure out why this happens and maybe delete it.
 		if (stderr.indexOf('unexpected directory layout:') > -1) {
-			await execFile(goRuntimePath, args, opts);
+			await execFile(goVersion.binaryPath, args, opts);
 		} else if (hasModSuffix(tool)) {
 			const gopath = env['GOPATH'];
 			if (!gopath) {
 				return `GOPATH not configured in environment`;
 			}
 			const outputFile = path.join(gopath, 'bin', process.platform === 'win32' ? `${tool.name}.exe` : tool.name);
-			await execFile(goRuntimePath, ['build', '-o', outputFile, importPath], opts);
+			await execFile(goVersion.binaryPath, ['build', '-o', outputFile, importPath], opts);
 		}
 		outputChannel.appendLine(`Installing ${importPath} SUCCEEDED`);
 	} catch (e) {
@@ -266,6 +259,9 @@
 	}
 
 	const goVersion = await getGoVersion();
+	if (!goVersion) {
+		return;
+	}
 
 	// Show error messages for outdated tools or outdated Go versions.
 	if (tool.minimumGoVersion && goVersion.lt(tool.minimumGoVersion.format())) {
diff --git a/src/util.ts b/src/util.ts
index 13a2f53..0561001 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -9,6 +9,7 @@
 import path = require('path');
 import semver = require('semver');
 import kill = require('tree-kill');
+import util = require('util');
 import vscode = require('vscode');
 import { NearestNeighborDict, Node } from './avlTree';
 import { toolExecutionEnvironment } from './goEnv';
@@ -78,21 +79,28 @@
 ]);
 
 export class GoVersion {
-	public sv: semver.SemVer;
-	public isDevel: boolean;
-	private commit: string;
+	public sv?: semver.SemVer;
+	public isDevel?: boolean;
+	private commit?: string;
 
-	constructor(version: string) {
+	constructor(public binaryPath: string, version: string) {
 		const matchesRelease = /go version go(\d.\d+).*/.exec(version);
 		const matchesDevel = /go version devel \+(.[a-zA-Z0-9]+).*/.exec(version);
 		if (matchesRelease) {
-			this.sv = semver.coerce(matchesRelease[0]);
+			const sv = semver.coerce(matchesRelease[0]);
+			if (sv) {
+				this.sv = sv;
+			}
 		} else if (matchesDevel) {
 			this.isDevel = true;
 			this.commit = matchesDevel[0];
 		}
 	}
 
+	public isValid(): boolean {
+		return !!this.sv || !!this.isDevel;
+	}
+
 	public format(): string {
 		if (this.sv) {
 			return this.sv.format();
@@ -106,7 +114,11 @@
 		if (this.isDevel || !this.sv) {
 			return false;
 		}
-		return semver.lt(this.sv, semver.coerce(version));
+		const v = semver.coerce(version);
+		if (!v) {
+			return false;
+		}
+		return semver.lt(this.sv, v);
 	}
 
 	public gt(version: string): boolean {
@@ -115,12 +127,16 @@
 		if (this.isDevel || !this.sv) {
 			return true;
 		}
-		return semver.gt(this.sv, semver.coerce(version));
+		const v = semver.coerce(version);
+		if (!v) {
+			return false;
+		}
+		return semver.gt(this.sv, v);
 	}
 }
 
-let cachedGoVersion: GoVersion = null;
-let vendorSupport: boolean = null;
+let cachedGoVersion: GoVersion | undefined;
+let vendorSupport: boolean | undefined;
 let toolsGopath: string;
 
 export function getGoConfig(uri?: vscode.Uri): vscode.WorkspaceConfiguration {
@@ -282,33 +298,37 @@
  * Gets version of Go based on the output of the command `go version`.
  * Returns null if go is being used from source/tip in which case `go version` will not return release tag like go1.6.3
  */
-export async function getGoVersion(): Promise<GoVersion> {
+export async function getGoVersion(): Promise<GoVersion | undefined> {
 	const goRuntimePath = getBinPath('go');
 
+	const warn = (msg: string) => {
+		outputChannel.appendLine(msg);
+		console.warn(msg);
+	};
+
 	if (!goRuntimePath) {
-		console.warn(
-			`Failed to run "go version" as the "go" binary cannot be found in either GOROOT(${process.env['GOROOT']}) or PATH(${envPath})`
-		);
-		return Promise.resolve(null);
+		warn(`unable to locate "go" binary in GOROOT (${process.env['GOROOT']}) or PATH (${envPath})`);
+		return;
 	}
-	if (cachedGoVersion && (cachedGoVersion.sv || cachedGoVersion.isDevel)) {
-		return Promise.resolve(cachedGoVersion);
+	if (cachedGoVersion) {
+		if (cachedGoVersion.isValid()) {
+			return Promise.resolve(cachedGoVersion);
+		}
+		warn(`cached Go version (${cachedGoVersion}) is invalid, recomputing`);
 	}
-	return new Promise<GoVersion>((resolve) => {
-		cp.execFile(goRuntimePath, ['version'], {}, (err, stdout, stderr) => {
-			cachedGoVersion = new GoVersion(stdout);
-			if (!cachedGoVersion.sv && !cachedGoVersion.isDevel) {
-				if (err || stderr) {
-					console.log(`Error when running the command "${goRuntimePath} version": `, err || stderr);
-				} else {
-					console.log(
-						`Not able to determine version from the output of the command "${goRuntimePath} version": ${stdout}`
-					);
-				}
-			}
-			return resolve(cachedGoVersion);
-		});
-	});
+	try {
+		const execFile = util.promisify(cp.execFile);
+		const { stdout, stderr } = await execFile(goRuntimePath, ['version']);
+		if (stderr) {
+			warn(`failed to run "${goRuntimePath} version": stdout: ${stdout}, stderr: ${stderr}`);
+			return;
+		}
+		cachedGoVersion = new GoVersion(goRuntimePath, stdout);
+	} catch (err) {
+		warn(`failed to run "${goRuntimePath} version": ${err}`);
+		return;
+	}
+	return cachedGoVersion;
 }
 
 /**