Skip to content

Commit d88ee9b

Browse files
committed
src/goDebugFactory: respond with error if dlv dap failed to start
DelveDAPOutputAdapter constructor begins spawning of dlv dap process but it doesn't wait until the dlv dap process is up and running. The success or failure of dlv dap process launch is lazily detected when the client sends messages. Previously, we just sent a TerminatedEvent and pop up an error notification. This CL changes DelveDAPOutputAdapter to respond with DebugProtocol.ErrorResponse, so the client can handle the error - displaying the error with a modal popup - changing focus so DEBUG CONSOLE becomes visible And made startAndConnectToServer catches errors and never throw an error (reject the promise). So, DelveDAPOutputAdapter's 'connected' will never be rejected and explicitly store the error message. Previously this rejected promise caused warnings, i.e., rejected promise not handled within X seconds. Did some clean up of unnecessary async or declaration. And, fixed and reenabled logDest tests. Change-Id: I57b7d52d83a82c506629e07b1dec65f9e02801bc Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/321472 Trust: Hyang-Ah Hana Kim <[email protected]> Trust: Suzy Mueller <[email protected]> Run-TryBot: Hyang-Ah Hana Kim <[email protected]> TryBot-Result: kokoro <[email protected]> Reviewed-by: Suzy Mueller <[email protected]>
1 parent fe7b40b commit d88ee9b

File tree

2 files changed

+78
-74
lines changed

2 files changed

+78
-74
lines changed

src/goDebugFactory.ts

Lines changed: 74 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as fs from 'fs';
1515
import * as net from 'net';
1616
import { getTool } from './goTools';
1717
import { Logger, TimestampedLogger } from './goLogging';
18+
import { DebugProtocol } from 'vscode-debugprotocol';
1819

1920
export class GoDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory {
2021
constructor(private outputChannel?: vscode.OutputChannel) {}
@@ -198,30 +199,35 @@ export class DelveDAPOutputAdapter extends ProxyDebugAdapter {
198199
this.connected = this.startAndConnectToServer();
199200
}
200201

201-
private connected: Promise<void>;
202+
private connected: Promise<{ connected: boolean; reason?: any }>;
202203
private dlvDapServer: ChildProcess;
203204
private port: number;
204205
private socket: net.Socket;
205206
private terminatedOnError = false;
206207

207208
protected async sendMessageToServer(message: vscode.DebugProtocolMessage): Promise<void> {
208-
try {
209-
await this.connected;
209+
const { connected, reason } = await this.connected;
210+
if (connected) {
210211
super.sendMessageToServer(message);
211-
} catch (err) {
212-
if (this.terminatedOnError) {
213-
return;
214-
}
212+
return;
213+
}
214+
const errMsg = `Couldn't start dlv dap:\n${reason}`;
215+
if (this.terminatedOnError) {
215216
this.terminatedOnError = true;
216-
// If there was an error connecting, show an error message and send a terminated event
217-
// since we cannot start.
218-
if (err) {
219-
const errMsg = `Debug Error: ${err}`;
220-
this.outputEvent('stderr', errMsg);
221-
vscode.window.showErrorMessage(errMsg);
222-
}
217+
this.outputEvent('stderr', errMsg);
223218
this.sendMessageToClient(new TerminatedEvent());
224219
}
220+
if ((message as any).type === 'request') {
221+
const req = message as DebugProtocol.Request;
222+
this.sendMessageToClient({
223+
seq: 0,
224+
type: 'response',
225+
request_seq: req.seq,
226+
success: false,
227+
command: req.command,
228+
message: errMsg
229+
});
230+
}
225231
}
226232

227233
async dispose(timeoutMS?: number) {
@@ -268,48 +274,54 @@ export class DelveDAPOutputAdapter extends ProxyDebugAdapter {
268274
}
269275

270276
private async startAndConnectToServer() {
271-
const { port, host, dlvDapServer } = await startDapServer(
272-
this.config,
273-
(msg) => this.outputEvent('stdout', msg),
274-
(msg) => this.outputEvent('stderr', msg),
275-
(msg) => {
276-
this.outputEvent('console', msg);
277-
// Some log messages generated after vscode stops the debug session
278-
// may not appear in the DEBUG CONSOLE. For easier debugging, log
279-
// the messages through the logger that prints to Go Debug output
280-
// channel.
281-
this.logger?.info(msg);
282-
}
283-
);
284-
const socket = await new Promise<net.Socket>((resolve, reject) => {
285-
// eslint-disable-next-line prefer-const
286-
let timer: NodeJS.Timeout;
287-
const s = net.createConnection(port, host, () => {
288-
clearTimeout(timer);
289-
resolve(s);
277+
try {
278+
const { port, host, dlvDapServer } = await startDapServer(
279+
this.config,
280+
(msg) => this.outputEvent('stdout', msg),
281+
(msg) => this.outputEvent('stderr', msg),
282+
(msg) => {
283+
this.outputEvent('console', msg);
284+
// Some log messages generated after vscode stops the debug session
285+
// may not appear in the DEBUG CONSOLE. For easier debugging, log
286+
// the messages through the logger that prints to Go Debug output
287+
// channel.
288+
this.logger?.info(msg);
289+
}
290+
);
291+
const socket = await new Promise<net.Socket>((resolve, reject) => {
292+
// eslint-disable-next-line prefer-const
293+
let timer: NodeJS.Timeout;
294+
const s = net.createConnection(port, host, () => {
295+
clearTimeout(timer);
296+
resolve(s);
297+
});
298+
timer = setTimeout(() => {
299+
reject('connection timeout');
300+
s?.destroy();
301+
}, 1000);
290302
});
291-
timer = setTimeout(() => {
292-
reject('connection timeout');
293-
s?.destroy();
294-
}, 1000);
295-
});
296303

297-
this.dlvDapServer = dlvDapServer;
298-
this.port = port;
299-
this.socket = socket;
300-
this.start(this.socket, this.socket);
304+
this.dlvDapServer = dlvDapServer;
305+
this.port = port;
306+
this.socket = socket;
307+
this.start(this.socket, this.socket);
308+
} catch (err) {
309+
return { connected: false, reason: err };
310+
}
311+
this.logger?.debug(`Running dlv dap server: port=${this.port} pid=${this.dlvDapServer.pid}\n`);
312+
return { connected: true };
301313
}
302314

303315
private outputEvent(dest: string, output: string, data?: any) {
304316
this.sendMessageToClient(new OutputEvent(output, dest, data));
305317
}
306318
}
307319

308-
export async function startDapServer(
320+
async function startDapServer(
309321
configuration: vscode.DebugConfiguration,
310-
log?: (msg: string) => void,
311-
logErr?: (msg: string) => void,
312-
logConsole?: (msg: string) => void
322+
log: (msg: string) => void,
323+
logErr: (msg: string) => void,
324+
logConsole: (msg: string) => void
313325
): Promise<{ port: number; host: string; dlvDapServer?: ChildProcessWithoutNullStreams }> {
314326
const host = configuration.host || '127.0.0.1';
315327

@@ -319,20 +331,11 @@ export async function startDapServer(
319331
return { port: configuration.port, host };
320332
}
321333
const port = await getPort();
322-
if (!log) {
323-
log = appendToDebugConsole;
324-
}
325-
if (!logErr) {
326-
logErr = appendToDebugConsole;
327-
}
328-
if (!logConsole) {
329-
logConsole = appendToDebugConsole;
330-
}
331334
const dlvDapServer = await spawnDlvDapServerProcess(configuration, host, port, log, logErr, logConsole);
332335
return { dlvDapServer, port, host };
333336
}
334337

335-
async function spawnDlvDapServerProcess(
338+
function spawnDlvDapServerProcess(
336339
launchArgs: vscode.DebugConfiguration,
337340
host: string,
338341
port: number,
@@ -350,12 +353,19 @@ async function spawnDlvDapServerProcess(
350353
logErr(
351354
`Couldn't find dlv-dap at the Go tools path, ${process.env['GOPATH']}${
352355
env['GOPATH'] ? ', ' + env['GOPATH'] : ''
353-
} or ${envPath}`
354-
);
355-
throw new Error(
356-
'Cannot find Delve debugger. Install from https://github.com/go-delve/delve & ensure it is in your Go tools path, "GOPATH/bin" or "PATH".'
356+
} or ${envPath}\n` +
357+
'Follow the setup instruction in https://github.com/golang/vscode-go/blob/master/docs/dlv-dap.md#getting-started.\n'
357358
);
359+
throw new Error('Cannot find Delve debugger (dlv dap)');
358360
}
361+
let dir = '';
362+
try {
363+
dir = parseProgramArgSync(launchArgs).dirname;
364+
} catch (err) {
365+
logErr(`Program arg: ${launchArgs.program}\n${err}\n`);
366+
throw err; // rethrow so the caller knows it failed.
367+
}
368+
359369
const dlvArgs = new Array<string>();
360370
dlvArgs.push('dap');
361371
// add user-specified dlv flags first. When duplicate flags are specified,
@@ -379,12 +389,12 @@ async function spawnDlvDapServerProcess(
379389

380390
const logDest = launchArgs.logDest;
381391
if (typeof logDest === 'number') {
382-
logErr('Using a file descriptor for `logDest` is not allowed.');
392+
logErr(`Using a file descriptor for 'logDest' (${logDest}) is not allowed.\n`);
383393
throw new Error('Using a file descriptor for `logDest` is not allowed.');
384394
}
385395
if (logDest && !path.isAbsolute(logDest)) {
386396
logErr(
387-
'Using a relative path for `logDest` is not allowed.\nSee [variables](https://code.visualstudio.com/docs/editor/variables-reference)'
397+
`Using a relative path for 'logDest' (${logDest}) is not allowed.\nSee https://code.visualstudio.com/docs/editor/variables-reference if you want workspace-relative path.\n`
388398
);
389399
throw new Error('Using a relative path for `logDest` is not allowed');
390400
}
@@ -397,14 +407,13 @@ async function spawnDlvDapServerProcess(
397407

398408
const logDestStream = logDest ? fs.createWriteStream(logDest) : undefined;
399409

400-
logConsole(`Running: ${dlvPath} ${dlvArgs.join(' ')}\n`);
410+
logConsole(`Starting: ${dlvPath} ${dlvArgs.join(' ')}\n`);
401411

402-
const dir = parseProgramArgSync(launchArgs).dirname;
403412
// TODO(hyangah): determine the directories:
404413
// run `dlv` => where dlv will create the default __debug_bin. (This won't work if the directory is not writable. Fix it)
405414
// build program => 'program' directory. (This won't work for multimodule workspace. Fix it)
406415
// run program => cwd (If test, make sure to run in the package directory.)
407-
return await new Promise<ChildProcess>((resolve, reject) => {
416+
return new Promise<ChildProcess>((resolve, reject) => {
408417
const p = spawn(dlvPath, dlvArgs, {
409418
cwd: dir,
410419
env,
@@ -521,8 +530,3 @@ export function parseProgramArgSync(
521530
const dirname = programIsDirectory ? program : path.dirname(program);
522531
return { program, dirname, programIsDirectory };
523532
}
524-
525-
// appendToDebugConsole is declared as an exported const rather than a function, so it can be stubbbed in testing.
526-
export const appendToDebugConsole = (msg: string) => {
527-
console.error(msg);
528-
};

test/integration/goDebug.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,7 +1760,7 @@ const testAll = (ctx: Mocha.Context, isDlvDap: boolean) => {
17601760
});
17611761
});
17621762

1763-
suite.skip('logDest attribute tests', () => {
1763+
suite('logDest attribute tests', () => {
17641764
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
17651765

17661766
let tmpDir: string;
@@ -1808,13 +1808,13 @@ const testAll = (ctx: Mocha.Context, isDlvDap: boolean) => {
18081808
logDest
18091809
};
18101810

1811+
await initializeDebugConfig(config);
18111812
try {
1812-
await initializeDebugConfig(config);
1813+
await dc.initializeRequest();
1814+
assert.fail('dlv dap started normally, wanted the invalid logDest to cause failure');
18131815
} catch (error) {
18141816
assert(error?.message.includes(wantedErrorMessage), `unexpected error: ${error}`);
1815-
return;
18161817
}
1817-
assert.fail('dlv dap started normally, wanted the invalid logDest to cause failure');
18181818
}
18191819
test('relative path as logDest triggers an error', async function () {
18201820
if (!isDlvDap || process.platform === 'win32') this.skip();

0 commit comments

Comments
 (0)