blob: 5ebdef0d0874fffd73f176277c113a4f21a3cceb [file] [log] [blame]
/* eslint-disable @typescript-eslint/no-explicit-any */
/*---------------------------------------------------------
* Copyright 2021 The Go Authors. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
import { ChildProcess, ChildProcessWithoutNullStreams, spawn } from 'child_process';
import * as fs from 'fs';
import { DebugConfiguration } from 'vscode';
import { envPath } from './utils/pathUtils';
import { killProcessTree } from './utils/processUtils';
import getPort = require('get-port');
import path = require('path');
import vscode = require('vscode');
export class GoDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory {
private dlvDapServer?: ChildProcess;
public async createDebugAdapterDescriptor(
session: vscode.DebugSession,
executable: vscode.DebugAdapterExecutable | undefined
): Promise<vscode.ProviderResult<vscode.DebugAdapterDescriptor>> {
if (session.configuration.debugAdapter === 'dlv-dap') {
return this.createDebugAdapterDescriptorDlvDap(session.configuration);
}
// Terminate any running dlv dap server process.
await this.terminateDlvDapServerProcess();
return executable;
}
public async dispose() {
await this.terminateDlvDapServerProcess();
}
private async createDebugAdapterDescriptorDlvDap(
configuration: vscode.DebugConfiguration
): 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, dlvDapServer } = await startDapServer(configuration);
this.dlvDapServer = dlvDapServer;
return new vscode.DebugAdapterServer(port, host);
}
private async terminateDlvDapServerProcess() {
if (this.dlvDapServer) {
await killProcessTree(this.dlvDapServer);
this.dlvDapServer = null;
}
}
}
export async function startDapServer(
configuration: DebugConfiguration
): Promise<{ port: number; host: string; dlvDapServer?: ChildProcessWithoutNullStreams }> {
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();
}
const dlvDapServer = await spawnDlvDapServerProcess(configuration);
return { dlvDapServer, port: configuration.port, host: configuration.host };
}
async function spawnDlvDapServerProcess(launchArgs: DebugConfiguration): Promise<ChildProcess> {
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)) {
appendToDebugConsole(
`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);
}
appendToDebugConsole(`Running: ${dlvPath} ${dlvArgs.join(' ')}`);
const dir = parseProgramArgSync(launchArgs).dirname;
return await new Promise<ChildProcess>((resolve, reject) => {
const p = spawn(dlvPath, dlvArgs, {
cwd: dir,
env
});
let started = false;
const timeoutToken: NodeJS.Timer = setTimeout(
() => reject(new Error('timed out while waiting for DAP server to start')),
5_000
);
const stopWaitingForServerToStart = (err?: string) => {
clearTimeout(timeoutToken);
started = true;
if (err) {
killProcessTree(p); // We do not need to wait for p to actually be killed.
reject(new Error(err));
} else {
resolve(p);
}
};
p.stdout.on('data', (chunk) => {
if (!started) {
if (chunk.toString().startsWith('DAP server listening at:')) {
stopWaitingForServerToStart();
} else {
stopWaitingForServerToStart(
`Expected 'DAP server listening at:' from debug adapter got '${chunk.toString()}'`
);
}
}
appendToDebugConsole(chunk.toString());
});
p.stderr.on('data', (chunk) => {
if (!started) {
stopWaitingForServerToStart(`Unexpected error from dlv dap on start: '${chunk.toString()}'`);
}
appendToDebugConsole(chunk.toString());
});
p.on('close', (code) => {
if (!started) {
stopWaitingForServerToStart(`dlv dap closed with code: '${code}' signal: ${p.killed}`);
}
if (code) {
appendToDebugConsole(`Process exiting with code: ${code} signal: ${p.killed}`);
} else {
appendToDebugConsole(`Process exited normally: ${p.killed}`);
}
});
p.on('error', (err) => {
if (!started) {
stopWaitingForServerToStart(`Unexpected error from dlv dap on start: '${err}'`);
}
if (err) {
appendToDebugConsole(`Error: ${err}`);
}
});
});
}
function parseProgramArgSync(
launchArgs: DebugConfiguration
): { program: string; dirname: string; programIsDirectory: boolean } {
const program = launchArgs.program;
if (!program) {
throw new Error('The program attribute is missing in the debug configuration in launch.json');
}
let programIsDirectory = false;
try {
programIsDirectory = fs.lstatSync(program).isDirectory();
} catch (e) {
throw new Error('The program attribute must point to valid directory, .go file or executable.');
}
if (!programIsDirectory && launchArgs.mode !== 'exec' && path.extname(program) !== '.go') {
throw new Error('The program attribute must be a directory or .go file in debug and test mode');
}
const dirname = programIsDirectory ? program : path.dirname(program);
return { program, dirname, programIsDirectory };
}
function appendToDebugConsole(msg: string) {
// TODO(hyangah): use color distinguishable from the color used from print.
vscode.debug.activeDebugConsole.append(msg);
}