src: add handling for on-close behavior for tools

I doubt many other tools will have on-close behavior, but this feels a bit nicer. We can always delete the function if it becomes unnecessary. Unfortunately the diffs are not as nice as I had hoped because a few variables had to be moved around, but hopefully this isn't too bad.

Change-Id: I4905d19264859ea3b967dfec566402185ef00c00
GitHub-Last-Rev: ce5dd584514dd00fc5457cd66dd8c928de5223df
GitHub-Pull-Request: golang/vscode-go#40
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/233600
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts
index 52683b3..f86c6da 100644
--- a/src/goInstallTools.ts
+++ b/src/goInstallTools.ts
@@ -191,7 +191,7 @@
 		.reduce((res: Promise<string[]>, tool: ToolAtVersion) => {
 			return res.then(
 				(sofar) =>
-					new Promise<string[]>((resolve, reject) => {
+					new Promise<string[]>(async (resolve, reject) => {
 						// Disable modules for tools which are installed with the "..." wildcard.
 						// TODO: ... will be supported in Go 1.13, so enable these tools to use modules then.
 						const modulesOffForTool = modulesOff || disableModulesForWildcard(tool, goVersion);
@@ -204,17 +204,18 @@
 							tmpGoModFile = path.join(toolsTmpDir, 'go.mod');
 							fs.writeFileSync(tmpGoModFile, 'module tools');
 						}
+						let importPath: string;
+						if (modulesOffForTool) {
+							importPath = getImportPath(tool, goVersion);
+						} else {
+							importPath = getImportPathWithVersion(tool, tool.version, goVersion);
+						}
 
-						const opts = {
-							env: envForTools,
-							cwd: toolsTmpDir
-						};
 						const callback = (err: Error, stdout: string, stderr: string) => {
 							// Make sure to delete the temporary go.mod file, if it exists.
 							if (tmpGoModFile && fs.existsSync(tmpGoModFile)) {
 								fs.unlinkSync(tmpGoModFile);
 							}
-							const importPath = getImportPathWithVersion(tool, tool.version, goVersion);
 							if (err) {
 								outputChannel.appendLine('Installing ' + importPath + ' FAILED');
 								const failureReason = tool.name + ';;' + err + stdout.toString() + stderr.toString();
@@ -225,66 +226,51 @@
 							}
 						};
 
-						let closeToolPromise = Promise.resolve(true);
-						const toolBinPath = getBinPath(tool.name);
-						if (path.isAbsolute(toolBinPath) && isGocode(tool)) {
-							closeToolPromise = new Promise<boolean>((innerResolve) => {
-								cp.execFile(toolBinPath, ['close'], {}, (err, stdout, stderr) => {
-									if (stderr && stderr.indexOf(`rpc: can't find service Server.`) > -1) {
-										outputChannel.appendLine(
-											'Installing gocode aborted as existing process cannot be closed. Please kill the running process for gocode and try again.'
-										);
-										return innerResolve(false);
-									}
-									innerResolve(true);
-								});
-							});
-						}
-
-						closeToolPromise.then((success) => {
-							if (!success) {
+						// Perform any on-close actions before reinstalling the tool.
+						if (tool.close) {
+							const errMsg = await tool.close();
+							if (errMsg) {
+								outputChannel.appendLine(errMsg);
 								resolve([...sofar, null]);
 								return;
 							}
-							const args = ['get', '-v'];
-							// Only get tools at master if we are not using modules.
-							if (modulesOffForTool) {
-								args.push('-u');
-							}
-							// Tools with a "mod" suffix should not be installed,
-							// instead we run "go build -o" to rename them.
-							if (hasModSuffix(tool)) {
-								args.push('-d');
-							}
-							let importPath: string;
-							if (modulesOffForTool) {
-								importPath = getImportPath(tool, goVersion);
+						}
+						const args = ['get', '-v'];
+						// Only get tools at master if we are not using modules.
+						if (modulesOffForTool) {
+							args.push('-u');
+						}
+						// Tools with a "mod" suffix should not be installed,
+						// instead we run "go build -o" to rename them.
+						if (hasModSuffix(tool)) {
+							args.push('-d');
+						}
+						args.push(importPath);
+						const opts = {
+							env: envForTools,
+							cwd: toolsTmpDir
+						};
+						cp.execFile(goRuntimePath, args, opts, (err, stdout, stderr) => {
+							if (stderr.indexOf('unexpected directory layout:') > -1) {
+								outputChannel.appendLine(
+									`Installing ${importPath} failed with error "unexpected directory layout". Retrying...`
+								);
+								cp.execFile(goRuntimePath, args, opts, callback);
+							} else if (!err && hasModSuffix(tool)) {
+								const outputFile = path.join(
+									toolsGopath,
+									'bin',
+									process.platform === 'win32' ? `${tool.name}.exe` : tool.name
+								);
+								cp.execFile(
+									goRuntimePath,
+									['build', '-o', outputFile, getImportPath(tool, goVersion)],
+									opts,
+									callback
+								);
 							} else {
-								importPath = getImportPathWithVersion(tool, tool.version, goVersion);
+								callback(err, stdout, stderr);
 							}
-							args.push(importPath);
-							cp.execFile(goRuntimePath, args, opts, (err, stdout, stderr) => {
-								if (stderr.indexOf('unexpected directory layout:') > -1) {
-									outputChannel.appendLine(
-										`Installing ${importPath} failed with error "unexpected directory layout". Retrying...`
-									);
-									cp.execFile(goRuntimePath, args, opts, callback);
-								} else if (!err && hasModSuffix(tool)) {
-									const outputFile = path.join(
-										toolsGopath,
-										'bin',
-										process.platform === 'win32' ? `${tool.name}.exe` : tool.name
-									);
-									cp.execFile(
-										goRuntimePath,
-										['build', '-o', outputFile, getImportPath(tool, goVersion)],
-										opts,
-										callback
-									);
-								} else {
-									callback(err, stdout, stderr);
-								}
-							});
 						});
 					})
 			);
diff --git a/src/goTools.ts b/src/goTools.ts
index 7b6819b..1552c49 100644
--- a/src/goTools.ts
+++ b/src/goTools.ts
@@ -5,10 +5,13 @@
 
 'use strict';
 
+import cp = require('child_process');
 import moment = require('moment');
+import path = require('path');
 import semver = require('semver');
+import util = require('util');
 import { goLiveErrorsEnabled } from './goLiveErrors';
-import { getGoConfig, GoVersion } from './util';
+import { getBinPath, getGoConfig, GoVersion } from './util';
 
 export interface Tool {
 	name: string;
@@ -28,6 +31,11 @@
 	// Go with which this tool can be used.
 	minimumGoVersion?: semver.SemVer;
 	maximumGoVersion?: semver.SemVer;
+
+	// close performs any shutdown tasks that a tool must execute before a new
+	// version is installed. It returns a string containing an error message on
+	// failure.
+	close?: () => Promise<string>;
 }
 
 /**
@@ -169,6 +177,22 @@
 		importPath: 'github.com/mdempsky/gocode',
 		isImportant: true,
 		description: 'Auto-completion, does not work with modules',
+		close: async (): Promise<string> => {
+			const toolBinPath = getBinPath('gocode');
+			if (!path.isAbsolute(toolBinPath)) {
+				return '';
+			}
+			try {
+				const execFile = util.promisify(cp.execFile);
+				const { stderr } = await execFile(toolBinPath, ['close']);
+				if (stderr.indexOf(`rpc: can't find service Server.`) > -1) {
+					return `Installing gocode aborted as existing process cannot be closed. Please kill the running process for gocode and try again.`;
+				}
+			} catch (err) {
+				return `Failed to close gocode process: ${err}.`;
+			}
+			return '';
+		},
 	},
 	'gocode-gomod': {
 		name: 'gocode-gomod',