Skip to content

Commit 60e6719

Browse files
committed
debug: implement noDebug launches in debugAdapter2
Updates #23 Change-Id: Ia68eaa4c075471a35fe63d1b2e1fbd30a80e8f48 GitHub-Last-Rev: 8588d77 GitHub-Pull-Request: #313 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/241659 Reviewed-by: Hyang-Ah Hana Kim <[email protected]>
1 parent ab37ed9 commit 60e6719

File tree

1 file changed

+152
-7
lines changed

1 file changed

+152
-7
lines changed

src/debugAdapter2/goDlvDebug.ts

+152-7
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,22 @@ import * as fs from 'fs';
1111
import net = require('net');
1212
import * as os from 'os';
1313
import * as path from 'path';
14+
import kill = require('tree-kill');
1415

1516
import {
1617
logger,
1718
Logger,
1819
LoggingDebugSession,
20+
OutputEvent,
1921
TerminatedEvent
2022
} from 'vscode-debugadapter';
2123
import { DebugProtocol } from 'vscode-debugprotocol';
2224

23-
import { envPath } from '../goPath';
25+
import {
26+
envPath,
27+
getBinPathWithPreferredGopathGoroot,
28+
parseEnvFile
29+
} from '../goPath';
2430
import { DAPClient } from './dapClient';
2531

2632
interface LoadConfig {
@@ -133,7 +139,11 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
133139

134140
private logLevel: Logger.LogLevel = Logger.LogLevel.Error;
135141

136-
private dlvClient: DelveClient;
142+
private dlvClient: DelveClient = null;
143+
144+
// Child process used to track debugee launched without debugging (noDebug
145+
// mode). Either debugProcess or dlvClient are null.
146+
private debugProcess: ChildProcess = null;
137147

138148
public constructor() {
139149
super();
@@ -185,19 +195,35 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
185195
const logPath =
186196
this.logLevel !== Logger.LogLevel.Error ? path.join(os.tmpdir(), 'vscode-godlvdapdebug.txt') : undefined;
187197
logger.setup(this.logLevel, logPath);
188-
189198
log('launchRequest');
190199

200+
// In noDebug mode, we don't launch Delve.
201+
// TODO: this logic is currently organized for compatibility with the
202+
// existing DA. It's not clear what we should do in case noDebug is
203+
// set and mode isn't 'debug'. Sending an error response could be
204+
// a safe option.
205+
if (args.noDebug && args.mode === 'debug') {
206+
try {
207+
this.launchNoDebug(args);
208+
} catch (e) {
209+
logError(`launchNoDebug failed: "${e}"`);
210+
// TODO: define error constants
211+
// https://github.com/golang/vscode-go/issues/305
212+
this.sendErrorResponse(
213+
response,
214+
3000,
215+
`Failed to launch "${e}"`);
216+
}
217+
return;
218+
}
219+
191220
if (!args.port) {
192221
args.port = this.DEFAULT_DELVE_PORT;
193222
}
194223
if (!args.host) {
195224
args.host = this.DEFAULT_DELVE_HOST;
196225
}
197226

198-
// TODO: if this is a noDebug launch request, don't launch Delve;
199-
// instead, run the program directly.
200-
201227
this.dlvClient = new DelveClient(args);
202228

203229
this.dlvClient.on('stdout', (str) => {
@@ -214,6 +240,8 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
214240

215241
this.dlvClient.on('close', (rc) => {
216242
if (rc !== 0) {
243+
// TODO: define error constants
244+
// https://github.com/golang/vscode-go/issues/305
217245
this.sendErrorResponse(
218246
response,
219247
3000,
@@ -248,7 +276,35 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
248276
args: DebugProtocol.DisconnectArguments,
249277
request?: DebugProtocol.Request
250278
): void {
251-
this.dlvClient.send(request);
279+
log('DisconnectRequest');
280+
// How we handle DisconnectRequest depends on whether Delve was launched
281+
// at all.
282+
// * In noDebug node, the Go program was spawned directly without
283+
// debugging: this.debugProcess will be non-null, and this.dlvClient
284+
// will be null.
285+
// * Otherwise, Delve was spawned: this.debugProcess will be null, and
286+
// this.dlvClient will be non-null.
287+
if (this.debugProcess !== null) {
288+
log(`killing debugee (pid: ${this.debugProcess.pid})...`);
289+
290+
// Kill the debugee and notify the client when the killing is
291+
// completed, to ensure a clean shutdown sequence.
292+
killProcessTree(this.debugProcess).then(() => {
293+
super.disconnectRequest(response, args);
294+
log('DisconnectResponse');
295+
});
296+
} else if (this.dlvClient !== null) {
297+
// Forward this DisconnectRequest to Delve.
298+
this.dlvClient.send(request);
299+
} else {
300+
logError(`both debug process and dlv client are null`);
301+
// TODO: define all error codes as constants
302+
// https://github.com/golang/vscode-go/issues/305
303+
this.sendErrorResponse(
304+
response,
305+
3000,
306+
'Failed to disconnect: Check the debug console for details.');
307+
}
252308
}
253309

254310
protected terminateRequest(
@@ -537,6 +593,73 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
537593
): void {
538594
this.dlvClient.send(request);
539595
}
596+
597+
// Launch the debugee process without starting a debugger.
598+
// This implements the `Run > Run Without Debugger` functionality in vscode.
599+
// Note: this method currently assumes launchArgs.mode === 'debug'.
600+
private launchNoDebug(launchArgs: LaunchRequestArguments): void {
601+
const program = launchArgs.program;
602+
if (!program) {
603+
throw new Error('The program attribute is missing in the debug configuration in launch.json');
604+
}
605+
let programIsDirectory = false;
606+
try {
607+
programIsDirectory = fs.lstatSync(program).isDirectory();
608+
} catch (e) {
609+
throw new Error('The program attribute must point to valid directory, .go file or executable.');
610+
}
611+
if (!programIsDirectory && path.extname(program) !== '.go') {
612+
throw new Error('The program attribute must be a directory or .go file in debug mode');
613+
}
614+
615+
const goRunArgs = ['run'];
616+
if (launchArgs.buildFlags) {
617+
goRunArgs.push(launchArgs.buildFlags);
618+
}
619+
620+
if (programIsDirectory) {
621+
goRunArgs.push('.');
622+
} else {
623+
goRunArgs.push(program);
624+
}
625+
626+
if (launchArgs.args) {
627+
goRunArgs.push(...launchArgs.args);
628+
}
629+
630+
// Read env from disk and merge into env variables.
631+
const fileEnvs = [];
632+
if (typeof launchArgs.envFile === 'string') {
633+
fileEnvs.push(parseEnvFile(launchArgs.envFile));
634+
}
635+
if (Array.isArray(launchArgs.envFile)) {
636+
launchArgs.envFile.forEach((envFile) => {
637+
fileEnvs.push(parseEnvFile(envFile));
638+
});
639+
}
640+
641+
const launchArgsEnv = launchArgs.env || {};
642+
const programEnv = Object.assign({}, process.env, ...fileEnvs, launchArgsEnv);
643+
644+
const dirname = programIsDirectory ? program : path.dirname(program);
645+
const goExe = getBinPathWithPreferredGopathGoroot('go', []);
646+
log(`Current working directory: ${dirname}`);
647+
log(`Running: ${goExe} ${goRunArgs.join(' ')}`);
648+
649+
this.debugProcess = spawn(goExe, goRunArgs, {
650+
cwd: dirname,
651+
env: programEnv
652+
});
653+
this.debugProcess.stderr.on('data', (str) => {
654+
this.sendEvent(new OutputEvent(str.toString(), 'stderr'));
655+
});
656+
this.debugProcess.stdout.on('data', (str) => {
657+
this.sendEvent(new OutputEvent(str.toString(), 'stdout'));
658+
});
659+
this.debugProcess.on('close', (rc) => {
660+
this.sendEvent(new TerminatedEvent());
661+
});
662+
}
540663
}
541664

542665
// DelveClient provides a DAP client to talk to a DAP server in Delve.
@@ -643,3 +766,25 @@ class DelveClient extends DAPClient {
643766
}, 200);
644767
}
645768
}
769+
770+
// TODO: refactor this function into util.ts so it could be reused with
771+
// the existing DA. Problem: it currently uses log() and logError() which makes
772+
// this more difficult.
773+
// We'll want a separate util.ts for the DA, because the current utils.ts pulls
774+
// in vscode as a dependency, which shouldn't be done in a DA.
775+
function killProcessTree(p: ChildProcess): Promise<void> {
776+
if (!p || !p.pid) {
777+
log(`no process to kill`);
778+
return Promise.resolve();
779+
}
780+
return new Promise((resolve) => {
781+
kill(p.pid, (err) => {
782+
if (err) {
783+
logError(`Error killing process ${p.pid}: ${err}`);
784+
} else {
785+
log(`killed process ${p.pid}`);
786+
}
787+
resolve();
788+
});
789+
});
790+
}

0 commit comments

Comments
 (0)