src/goDebugFactory.ts: run dlv-dap as a server and connect as server

Connect to the dlv dap server directly from vscode, bypassing the
thin adapter.

This is accomplished using the vscode.DebugAdapterDescriptorFactory
for launch configurations of type "godlvdap".

Updates golang/vscode-go#23
Updates golang/vscode-go#978

Change-Id: I877a1c1b0cf0c40a2ba0602ed1e90a27d8f0159e
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/288954
Trust: Suzy Mueller <suzmue@golang.org>
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/package.json b/package.json
index fcadcd0..96d7f67 100644
--- a/package.json
+++ b/package.json
@@ -891,7 +891,6 @@
       {
         "type": "godlvdap",
         "label": "Go Dlv Dap (Experimental)",
-        "program": "./dist/debugAdapter2.js",
         "runtime": "node",
         "languages": [
           "go"
diff --git a/src/debugAdapter2/goDlvDebug.ts b/src/debugAdapter2/goDlvDebug.ts
index d8ee451..0226492 100644
--- a/src/debugAdapter2/goDlvDebug.ts
+++ b/src/debugAdapter2/goDlvDebug.ts
@@ -11,6 +11,7 @@
 import net = require('net');
 import * as os from 'os';
 import * as path from 'path';
+import { DebugConfiguration } from 'vscode';
 
 import {
 	logger,
@@ -39,7 +40,9 @@
 }
 
 // This interface should always match the schema found in `package.json`.
-interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
+export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
+	type: string;
+	name: string;
 	request: 'launch';
 	[key: string]: any;
 	program: string;
@@ -652,48 +655,8 @@
 	constructor(launchArgs: LaunchRequestArguments) {
 		super();
 
-		const launchArgsEnv = launchArgs.env || {};
-		const env = Object.assign({}, process.env, launchArgsEnv);
-
-		// Let users override direct path to delve by setting it in the env
-		// map in launch.json; if unspecified, fall back to dlvToolPath.
-		let dlvPath = launchArgsEnv['dlvPath'];
-		if (!dlvPath) {
-			dlvPath = launchArgs.dlvToolPath;
-		}
-
-		if (!fs.existsSync(dlvPath)) {
-			log(
-				`Couldn't find dlv at the Go tools path, ${process.env['GOPATH']}${
-				env['GOPATH'] ? ', ' + env['GOPATH'] : ''
-				} or ${envPath}`
-			);
-			throw new Error(
-				`Cannot find Delve debugger. Install from https://github.com/go-delve/delve/ & ensure it is in your Go tools path, "GOPATH/bin" or "PATH".`
-			);
-		}
-
-		const dlvArgs = new Array<string>();
-		dlvArgs.push('dap');
-		// add user-specified dlv flags first. When duplicate flags are specified,
-		// dlv doesn't mind but accepts the last flag value.
-		if (launchArgs.dlvFlags && launchArgs.dlvFlags.length > 0) {
-			dlvArgs.push(...launchArgs.dlvFlags);
-		}
-		dlvArgs.push(`--listen=${launchArgs.host}:${launchArgs.port}`);
-		if (launchArgs.showLog) {
-			dlvArgs.push('--log=' + launchArgs.showLog.toString());
-		}
-		if (launchArgs.logOutput) {
-			dlvArgs.push('--log-output=' + launchArgs.logOutput);
-		}
-		log(`Running: ${dlvPath} ${dlvArgs.join(' ')}`);
-
 		const dir = parseProgramArgSync(launchArgs).dirname;
-		this.debugProcess = spawn(dlvPath, dlvArgs, {
-			cwd: dir,
-			env
-		});
+		this.debugProcess = spawnDapServerProcess(launchArgs, log, logError);
 
 		this.debugProcess.stderr.on('data', (chunk) => {
 			let str = chunk.toString();
@@ -759,7 +722,7 @@
 //
 // Throws an exception in case args.program is not a valid file or directory.
 // This function can block because it calls a blocking fs function.
-function parseProgramArgSync(launchArgs: LaunchRequestArguments
+export function parseProgramArgSync(launchArgs: DebugConfiguration
 ): { program: string, dirname: string, programIsDirectory: boolean } {
 	const program = launchArgs.program;
 	if (!program) {
@@ -777,3 +740,52 @@
 	const dirname = programIsDirectory ? program : path.dirname(program);
 	return {program, dirname, programIsDirectory};
 }
+
+export function spawnDapServerProcess(
+	launchArgs: DebugConfiguration,
+	logFn: (...args: any[]) => void,
+	logErrFn: (...args: any[]) => void
+) {
+	const launchArgsEnv = launchArgs.env || {};
+	const env = Object.assign({}, process.env, launchArgsEnv);
+
+	// Let users override direct path to delve by setting it in the env
+	// map in launch.json; if unspecified, fall back to dlvToolPath.
+	let dlvPath = launchArgsEnv['dlvPath'];
+	if (!dlvPath) {
+		dlvPath = launchArgs.dlvToolPath;
+	}
+
+	if (!fs.existsSync(dlvPath)) {
+		logErrFn(
+			`Couldn't find dlv at the Go tools path, ${process.env['GOPATH']}${
+			env['GOPATH'] ? ', ' + env['GOPATH'] : ''
+			} or ${envPath}`
+		);
+		throw new Error(
+			`Cannot find Delve debugger. Install from https://github.com/go-delve/delve/ & ensure it is in your Go tools path, "GOPATH/bin" or "PATH".`
+		);
+	}
+
+	const dlvArgs = new Array<string>();
+	dlvArgs.push('dap');
+	// add user-specified dlv flags first. When duplicate flags are specified,
+	// dlv doesn't mind but accepts the last flag value.
+	if (launchArgs.dlvFlags && launchArgs.dlvFlags.length > 0) {
+		dlvArgs.push(...launchArgs.dlvFlags);
+	}
+	dlvArgs.push(`--listen=${launchArgs.host}:${launchArgs.port}`);
+	if (launchArgs.showLog) {
+		dlvArgs.push('--log=' + launchArgs.showLog.toString());
+	}
+	if (launchArgs.logOutput) {
+		dlvArgs.push('--log-output=' + launchArgs.logOutput);
+	}
+	logFn(`Running: ${dlvPath} ${dlvArgs.join(' ')}`);
+
+	const dir = parseProgramArgSync(launchArgs).dirname;
+	return spawn(dlvPath, dlvArgs, {
+		cwd: dir,
+		env
+	});
+}
diff --git a/src/goDebugFactory.ts b/src/goDebugFactory.ts
new file mode 100644
index 0000000..9ada182
--- /dev/null
+++ b/src/goDebugFactory.ts
@@ -0,0 +1,64 @@
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import { ChildProcess } from 'child_process';
+import getPort = require('get-port');
+import { DebugConfiguration } from 'vscode';
+import vscode = require('vscode');
+import { spawnDapServerProcess as spawnDlvDapServerProcess } from './debugAdapter2/goDlvDebug';
+import { logError, logInfo } from './goLogging';
+import { killProcessTree } from './utils/processUtils';
+
+export class GoDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory {
+
+	private dlvDapServer?: ChildProcess;
+
+	public async createDebugAdapterDescriptor(
+		session: vscode.DebugSession,
+		executable: vscode.DebugAdapterExecutable | undefined
+		): Promise<vscode.ProviderResult<vscode.DebugAdapterDescriptor>> {
+		// The dlv-dap server currently receives certain flags and arguments on startup
+		// and must be started in an appropriate folder for the program to be debugged.
+		// In order to support this, we kill the current dlv-dap server, and start a
+		// new one.
+		await this.terminateDlvDapServerProcess();
+
+		const {port, host} = await this.startDapServer(session.configuration);
+		return new vscode.DebugAdapterServer(port, host);
+	}
+
+	public async dispose() {
+		await this.terminateDlvDapServerProcess();
+	}
+
+	private async terminateDlvDapServerProcess() {
+		if (this.dlvDapServer) {
+			await killProcessTree(this.dlvDapServer);
+			this.dlvDapServer = null;
+		}
+	}
+
+	private async startDapServer(configuration: DebugConfiguration): Promise<{ port: number; host: string; }> {
+		if (!configuration.host) {
+			configuration.host = '127.0.0.1';
+		}
+
+		if (configuration.port) {
+			// If a port has been specified, assume there is an already
+			// running dap server to connect to.
+			return {port: configuration.port, host: configuration.host};
+		} else {
+			configuration.port = await getPort();
+		}
+
+		this.dlvDapServer = spawnDlvDapServerProcess(configuration, logInfo, logError);
+		// Wait to give dlv-dap a chance to start before returning.
+		return await
+			new Promise<{ port: number; host: string; }>((resolve) => setTimeout(() => {
+				resolve({port: configuration.port, host: configuration.host});
+			}, 500));
+	}
+
+}
diff --git a/src/goMain.ts b/src/goMain.ts
index 5f9ddf3..3a53c3f 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -19,6 +19,7 @@
 	toggleCoverageCurrentPackage, trackCodeCoverageRemovalOnFileChange, updateCodeCoverageDecorators
 } from './goCover';
 import { GoDebugConfigurationProvider } from './goDebugConfiguration';
+import { GoDebugAdapterDescriptorFactory } from './goDebugFactory';
 import { extractFunction, extractVariable } from './goDoctor';
 import { toolExecutionEnvironment } from './goEnv';
 import { chooseGoEnvironment, offerToInstallLatestGoVersion, setEnvironmentVariableCollection } from './goEnvironmentStatus';
@@ -196,6 +197,13 @@
 			return await pickGoProcess();
 		}));
 
+	const factory = new GoDebugAdapterDescriptorFactory();
+	ctx.subscriptions.push(
+		vscode.debug.registerDebugAdapterDescriptorFactory('godlvdap', factory));
+	if ('dispose' in factory) {
+		ctx.subscriptions.push(factory);
+	}
+
 	buildDiagnosticCollection = vscode.languages.createDiagnosticCollection('go');
 	ctx.subscriptions.push(buildDiagnosticCollection);
 	lintDiagnosticCollection = vscode.languages.createDiagnosticCollection('go-lint');