debug: implement noDebug launches in debugAdapter2
Updates golang/vscode-go#23
Change-Id: Ia68eaa4c075471a35fe63d1b2e1fbd30a80e8f48
GitHub-Last-Rev: 8588d77bdb7350950f45dec28d60bf4b1c519ca7
GitHub-Pull-Request: golang/vscode-go#313
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/241659
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/debugAdapter2/goDlvDebug.ts b/src/debugAdapter2/goDlvDebug.ts
index 09e094e..cc1363e 100644
--- a/src/debugAdapter2/goDlvDebug.ts
+++ b/src/debugAdapter2/goDlvDebug.ts
@@ -11,16 +11,22 @@
import net = require('net');
import * as os from 'os';
import * as path from 'path';
+import kill = require('tree-kill');
import {
logger,
Logger,
LoggingDebugSession,
+ OutputEvent,
TerminatedEvent
} from 'vscode-debugadapter';
import { DebugProtocol } from 'vscode-debugprotocol';
-import { envPath } from '../goPath';
+import {
+ envPath,
+ getBinPathWithPreferredGopathGoroot,
+ parseEnvFile
+} from '../goPath';
import { DAPClient } from './dapClient';
interface LoadConfig {
@@ -133,7 +139,11 @@
private logLevel: Logger.LogLevel = Logger.LogLevel.Error;
- private dlvClient: DelveClient;
+ private dlvClient: DelveClient = null;
+
+ // Child process used to track debugee launched without debugging (noDebug
+ // mode). Either debugProcess or dlvClient are null.
+ private debugProcess: ChildProcess = null;
public constructor() {
super();
@@ -185,9 +195,28 @@
const logPath =
this.logLevel !== Logger.LogLevel.Error ? path.join(os.tmpdir(), 'vscode-godlvdapdebug.txt') : undefined;
logger.setup(this.logLevel, logPath);
-
log('launchRequest');
+ // In noDebug mode, we don't launch Delve.
+ // TODO: this logic is currently organized for compatibility with the
+ // existing DA. It's not clear what we should do in case noDebug is
+ // set and mode isn't 'debug'. Sending an error response could be
+ // a safe option.
+ if (args.noDebug && args.mode === 'debug') {
+ try {
+ this.launchNoDebug(args);
+ } catch (e) {
+ logError(`launchNoDebug failed: "${e}"`);
+ // TODO: define error constants
+ // https://github.com/golang/vscode-go/issues/305
+ this.sendErrorResponse(
+ response,
+ 3000,
+ `Failed to launch "${e}"`);
+ }
+ return;
+ }
+
if (!args.port) {
args.port = this.DEFAULT_DELVE_PORT;
}
@@ -195,9 +224,6 @@
args.host = this.DEFAULT_DELVE_HOST;
}
- // TODO: if this is a noDebug launch request, don't launch Delve;
- // instead, run the program directly.
-
this.dlvClient = new DelveClient(args);
this.dlvClient.on('stdout', (str) => {
@@ -214,6 +240,8 @@
this.dlvClient.on('close', (rc) => {
if (rc !== 0) {
+ // TODO: define error constants
+ // https://github.com/golang/vscode-go/issues/305
this.sendErrorResponse(
response,
3000,
@@ -248,7 +276,35 @@
args: DebugProtocol.DisconnectArguments,
request?: DebugProtocol.Request
): void {
- this.dlvClient.send(request);
+ log('DisconnectRequest');
+ // How we handle DisconnectRequest depends on whether Delve was launched
+ // at all.
+ // * In noDebug node, the Go program was spawned directly without
+ // debugging: this.debugProcess will be non-null, and this.dlvClient
+ // will be null.
+ // * Otherwise, Delve was spawned: this.debugProcess will be null, and
+ // this.dlvClient will be non-null.
+ if (this.debugProcess !== null) {
+ log(`killing debugee (pid: ${this.debugProcess.pid})...`);
+
+ // Kill the debugee and notify the client when the killing is
+ // completed, to ensure a clean shutdown sequence.
+ killProcessTree(this.debugProcess).then(() => {
+ super.disconnectRequest(response, args);
+ log('DisconnectResponse');
+ });
+ } else if (this.dlvClient !== null) {
+ // Forward this DisconnectRequest to Delve.
+ this.dlvClient.send(request);
+ } else {
+ logError(`both debug process and dlv client are null`);
+ // TODO: define all error codes as constants
+ // https://github.com/golang/vscode-go/issues/305
+ this.sendErrorResponse(
+ response,
+ 3000,
+ 'Failed to disconnect: Check the debug console for details.');
+ }
}
protected terminateRequest(
@@ -537,6 +593,73 @@
): void {
this.dlvClient.send(request);
}
+
+ // Launch the debugee process without starting a debugger.
+ // This implements the `Run > Run Without Debugger` functionality in vscode.
+ // Note: this method currently assumes launchArgs.mode === 'debug'.
+ private launchNoDebug(launchArgs: LaunchRequestArguments): void {
+ 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 && path.extname(program) !== '.go') {
+ throw new Error('The program attribute must be a directory or .go file in debug mode');
+ }
+
+ const goRunArgs = ['run'];
+ if (launchArgs.buildFlags) {
+ goRunArgs.push(launchArgs.buildFlags);
+ }
+
+ if (programIsDirectory) {
+ goRunArgs.push('.');
+ } else {
+ goRunArgs.push(program);
+ }
+
+ if (launchArgs.args) {
+ goRunArgs.push(...launchArgs.args);
+ }
+
+ // Read env from disk and merge into env variables.
+ const fileEnvs = [];
+ if (typeof launchArgs.envFile === 'string') {
+ fileEnvs.push(parseEnvFile(launchArgs.envFile));
+ }
+ if (Array.isArray(launchArgs.envFile)) {
+ launchArgs.envFile.forEach((envFile) => {
+ fileEnvs.push(parseEnvFile(envFile));
+ });
+ }
+
+ const launchArgsEnv = launchArgs.env || {};
+ const programEnv = Object.assign({}, process.env, ...fileEnvs, launchArgsEnv);
+
+ const dirname = programIsDirectory ? program : path.dirname(program);
+ const goExe = getBinPathWithPreferredGopathGoroot('go', []);
+ log(`Current working directory: ${dirname}`);
+ log(`Running: ${goExe} ${goRunArgs.join(' ')}`);
+
+ this.debugProcess = spawn(goExe, goRunArgs, {
+ cwd: dirname,
+ env: programEnv
+ });
+ this.debugProcess.stderr.on('data', (str) => {
+ this.sendEvent(new OutputEvent(str.toString(), 'stderr'));
+ });
+ this.debugProcess.stdout.on('data', (str) => {
+ this.sendEvent(new OutputEvent(str.toString(), 'stdout'));
+ });
+ this.debugProcess.on('close', (rc) => {
+ this.sendEvent(new TerminatedEvent());
+ });
+ }
}
// DelveClient provides a DAP client to talk to a DAP server in Delve.
@@ -643,3 +766,25 @@
}, 200);
}
}
+
+// TODO: refactor this function into util.ts so it could be reused with
+// the existing DA. Problem: it currently uses log() and logError() which makes
+// this more difficult.
+// We'll want a separate util.ts for the DA, because the current utils.ts pulls
+// in vscode as a dependency, which shouldn't be done in a DA.
+function killProcessTree(p: ChildProcess): Promise<void> {
+ if (!p || !p.pid) {
+ log(`no process to kill`);
+ return Promise.resolve();
+ }
+ return new Promise((resolve) => {
+ kill(p.pid, (err) => {
+ if (err) {
+ logError(`Error killing process ${p.pid}: ${err}`);
+ } else {
+ log(`killed process ${p.pid}`);
+ }
+ resolve();
+ });
+ });
+}