Skip to content

Commit f8f5433

Browse files
committed
src/goDebugFactory: wait before killing the spawned dap server
VSCode disposes the debug adapter as soon as it receives the disconnect response or reaches timeout. On the other hand, dlv dap is doing some remaining cleanup after sending a disconnect response. Allow some time for dlv dap to finish the cleanup. And, do not destroy the socket (closing both read/write), but end the socket (closing write) when the proxy adapter's dispose is called. In order to make it clear which side of the socket we are closing, this CL changes ProxyDebugAdapter.start to take stream.Readable and stream.Writable instead of a single duplex stream. https://nodejs.org/api/stream.html#stream_writable_end_chunk_encoding_callback Change-Id: I00f5a7685f4cc5ab210496ac7af902a137362d9c Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/306589 Trust: Hyang-Ah Hana Kim <hyangah@gmail.com> Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com> TryBot-Result: kokoro <noreply+kokoro@google.com> Reviewed-by: Suzy Mueller <suzmue@golang.org>
1 parent e545b83 commit f8f5433

File tree

1 file changed

+51
-44
lines changed

1 file changed

+51
-44
lines changed

src/goDebugFactory.ts

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ const TWO_CRLF = '\r\n\r\n';
5151
// start method is called.
5252
export class ProxyDebugAdapter implements vscode.DebugAdapter {
5353
private messageEmitter = new vscode.EventEmitter<vscode.DebugProtocolMessage>();
54-
// connection to server.
55-
private conn?: stream.Duplex;
54+
// connection from/to server (= dlv dap)
55+
private readable?: stream.Readable;
56+
private writable?: stream.Writable;
5657

5758
constructor() {
5859
this.onDidSendMessage = this.messageEmitter.event;
@@ -63,13 +64,6 @@ export class ProxyDebugAdapter implements vscode.DebugAdapter {
6364
// listen on onDidSendMessage to receive messages.
6465
onDidSendMessage: vscode.Event<vscode.DebugProtocolMessage>;
6566
async handleMessage(message: vscode.DebugProtocolMessage): Promise<void> {
66-
// TODO(hyangah): dlv dap often terminates before us
67-
// receiving the disconnect response, which causes
68-
// vscode to hang forever. Either generate a disconnect
69-
// respond after timeout so vscode completes the normal
70-
// debug session teardown including the call to this
71-
// thin adapter's dispose() or change dlv dap not to
72-
// kill itself until the client connection is closed.
7367
await this.sendMessageToServer(message);
7468
}
7569

@@ -79,46 +73,45 @@ export class ProxyDebugAdapter implements vscode.DebugAdapter {
7973
}
8074
protected sendMessageToServer(message: vscode.DebugProtocolMessage): void {
8175
const json = JSON.stringify(message) ?? '';
82-
if (this.conn) {
83-
this.conn.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}${TWO_CRLF}${json}`, 'utf8', (err) => {
84-
if (err) {
85-
console.log(`error sending message: ${err}`);
86-
this.sendMessageToClient(new TerminatedEvent());
76+
if (this.writable) {
77+
this.writable.write(
78+
`Content-Length: ${Buffer.byteLength(json, 'utf8')}${TWO_CRLF}${json}`,
79+
'utf8',
80+
(err) => {
81+
if (err) {
82+
console.log(`error sending message: ${err}`);
83+
this.sendMessageToClient(new TerminatedEvent());
84+
}
8785
}
88-
});
86+
);
8987
} else {
9088
console.log(`stream is closed; dropping ${json}`);
9189
}
9290
}
9391

94-
public async start(server: stream.Duplex) {
95-
if (this.conn) {
92+
public async start(readable: stream.Readable, writable: stream.Writable) {
93+
if (this.readable || this.writable) {
9694
throw new Error('start was called more than once');
9795
}
98-
this.conn = server;
99-
this.conn.on('data', (data: Buffer) => {
96+
this.readable = readable;
97+
this.writable = writable;
98+
this.readable.on('data', (data: Buffer) => {
10099
this.handleDataFromServer(data);
101100
});
102-
this.conn.once('close', () => {
103-
console.log('stream closed');
104-
this.conn.destroy();
105-
this.conn = undefined;
101+
this.readable.once('close', () => {
102+
this.readable = undefined;
106103
});
107-
this.conn.on('error', (err) => {
108-
console.log(`stream error: ${err}`);
104+
this.readable.on('error', (err) => {
109105
if (err) {
106+
console.log(`stream error: ${err}`);
110107
this.sendMessageToClient(new OutputEvent(`socket to network closed: ${err}`, 'console'));
111108
}
112109
this.sendMessageToClient(new TerminatedEvent());
113110
});
114111
}
115112

116113
async dispose() {
117-
if (this.conn) {
118-
// TODO(hyangah): not sure if sleep is necessary.
119-
await sleep(500);
120-
this.conn?.destroy();
121-
}
114+
this.writable?.end(); // no more write.
122115
}
123116

124117
private rawData = Buffer.alloc(0);
@@ -163,10 +156,6 @@ export class ProxyDebugAdapter implements vscode.DebugAdapter {
163156
}
164157
}
165158

166-
function sleep(ms: number) {
167-
return new Promise((resolve) => setTimeout(resolve, ms));
168-
}
169-
170159
// DelveDAPOutputAdapter is a ProxyDebugAdapter that proxies between
171160
// VSCode and a dlv dap process spawned and managed by this adapter.
172161
// It turns the process's stdout/stderrr into OutputEvent.
@@ -189,12 +178,27 @@ export class DelveDAPOutputAdapter extends ProxyDebugAdapter {
189178
}
190179

191180
async dispose() {
192-
console.log(`DelveDAPOutputAdapter.dispose ${this.dlvDapServer?.pid}`);
193-
super.dispose();
194-
if (this.connected) {
195-
killProcessTree(this.dlvDapServer);
196-
this.connected = undefined;
181+
await super.dispose();
182+
183+
if (this.connected === undefined) {
184+
return;
197185
}
186+
this.connected = undefined;
187+
const dlvDapServer = this.dlvDapServer;
188+
if (dlvDapServer.exitCode !== null) {
189+
return;
190+
}
191+
await new Promise<void>((resolve) => {
192+
const exitTimeoutToken = setTimeout(() => {
193+
console.log(`killing dlv dap process(${dlvDapServer.pid}) after 1sec`);
194+
killProcessTree(dlvDapServer);
195+
resolve();
196+
}, 1_000);
197+
dlvDapServer.on('exit', () => {
198+
clearTimeout(exitTimeoutToken);
199+
resolve();
200+
});
201+
});
198202
}
199203

200204
private async startAndConnectToServer() {
@@ -212,16 +216,14 @@ export class DelveDAPOutputAdapter extends ProxyDebugAdapter {
212216
});
213217
timer = setTimeout(() => {
214218
reject('connection timeout');
215-
console.log('failed to connect within 1s');
216219
s?.destroy();
217-
killProcessTree(dlvDapServer);
218220
}, 1000);
219221
});
220222

221223
this.dlvDapServer = dlvDapServer;
222224
this.port = port;
223225
this.socket = socket;
224-
this.start(this.socket);
226+
this.start(this.socket, this.socket);
225227
}
226228

227229
stdoutEvent(output: string, data?: any) {
@@ -330,6 +332,12 @@ async function spawnDlvDapServerProcess(
330332

331333
p.stdout.on('data', (chunk) => {
332334
if (!started) {
335+
// TODO(hyangah): when --log-dest is specified, the following message
336+
// will be written to the log dest file, not stdout/stderr.
337+
// Either disable --log-dest, or take advantage of it, i.e.,
338+
// always pass a file descriptor to --log-dest, watch the file
339+
// descriptor to process the log output, and also swap os.Stdout/os.Stderr
340+
// in dlv dap for launch requests to generate proper OutputEvents.
333341
if (chunk.toString().startsWith('DAP server listening at:')) {
334342
stopWaitingForServerToStart();
335343
} else {
@@ -347,13 +355,12 @@ async function spawnDlvDapServerProcess(
347355
logErr(chunk.toString());
348356
});
349357
p.on('close', (code) => {
358+
// TODO: should we watch 'exit' instead?
350359
if (!started) {
351360
stopWaitingForServerToStart(`dlv dap closed with code: '${code}' signal: ${p.killed}`);
352361
}
353362
if (code) {
354363
logErr(`Process exiting with code: ${code} signal: ${p.killed}`);
355-
} else {
356-
log(`Process exited normally: ${p.killed}`);
357364
}
358365
});
359366
p.on('error', (err) => {

0 commit comments

Comments
 (0)