Skip to content

Commit b9c80b8

Browse files
authored
Merge pull request #3178 from code-asher/connections
Minor connections refactor
2 parents 4ed7ae9 + 16fc315 commit b9c80b8

File tree

6 files changed

+193
-108
lines changed

6 files changed

+193
-108
lines changed

lib/vscode/src/vs/base/parts/ipc/common/ipc.net.ts

+5
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,11 @@ export class PersistentProtocol implements IMessagePassingProtocol {
743743
}, Math.max(ProtocolConstants.KeepAliveTimeoutTime - timeSinceLastIncomingMsg, 0) + 5);
744744
}
745745

746+
// NOTE@coder: Set the socket without initiating a reconnect.
747+
public setSocket(socket: ISocket): void {
748+
this._socket = socket;
749+
}
750+
746751
public getSocket(): ISocket {
747752
return this._socket;
748753
}

lib/vscode/src/vs/server/entry.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { logger } from 'vs/server/node/logger';
66
import { enableCustomMarketplace } from 'vs/server/node/marketplace';
77
import { Vscode } from 'vs/server/node/server';
88

9-
setUnexpectedErrorHandler((error) => logger.warn(error instanceof Error ? error.message : error));
9+
setUnexpectedErrorHandler((error) => {
10+
logger.warn('Uncaught error', field('error', error instanceof Error ? error.message : error));
11+
});
1012
enableCustomMarketplace();
1113
proxyAgent.monkeyPatch(true);
1214

lib/vscode/src/vs/server/node/connection.ts

+78-47
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,49 @@ import * as cp from 'child_process';
33
import { VSBuffer } from 'vs/base/common/buffer';
44
import { Emitter } from 'vs/base/common/event';
55
import { FileAccess } from 'vs/base/common/network';
6-
import { ISocket } from 'vs/base/parts/ipc/common/ipc.net';
7-
import { WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
86
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
7+
import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection';
98
import { getNlsConfiguration } from 'vs/server/node/nls';
109
import { Protocol } from 'vs/server/node/protocol';
1110
import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
1211

1312
export abstract class Connection {
1413
private readonly _onClose = new Emitter<void>();
14+
/**
15+
* Fire when the connection is closed (not just disconnected). This should
16+
* only happen when the connection is offline and old or has an error.
17+
*/
1518
public readonly onClose = this._onClose.event;
1619
private disposed = false;
1720
private _offline: number | undefined;
1821

19-
public constructor(protected protocol: Protocol, public readonly token: string) {}
22+
protected readonly logger: Logger;
23+
24+
public constructor(
25+
protected readonly protocol: Protocol,
26+
public readonly name: string,
27+
) {
28+
this.logger = logger.named(
29+
this.name,
30+
field('token', this.protocol.options.reconnectionToken),
31+
);
32+
33+
this.logger.debug('Connecting...');
34+
this.onClose(() => this.logger.debug('Closed'));
35+
}
2036

2137
public get offline(): number | undefined {
2238
return this._offline;
2339
}
2440

25-
public reconnect(socket: ISocket, buffer: VSBuffer): void {
41+
public reconnect(protocol: Protocol): void {
42+
this.logger.debug('Reconnecting...');
2643
this._offline = undefined;
27-
this.doReconnect(socket, buffer);
44+
this.doReconnect(protocol);
2845
}
2946

30-
public dispose(): void {
47+
public dispose(reason?: string): void {
48+
this.logger.debug('Disposing...', field('reason', reason));
3149
if (!this.disposed) {
3250
this.disposed = true;
3351
this.doDispose();
@@ -36,6 +54,7 @@ export abstract class Connection {
3654
}
3755

3856
protected setOffline(): void {
57+
this.logger.debug('Disconnected');
3958
if (!this._offline) {
4059
this._offline = Date.now();
4160
}
@@ -44,29 +63,34 @@ export abstract class Connection {
4463
/**
4564
* Set up the connection on a new socket.
4665
*/
47-
protected abstract doReconnect(socket: ISocket, buffer: VSBuffer): void;
66+
protected abstract doReconnect(protcol: Protocol): void;
67+
68+
/**
69+
* Dispose/destroy everything permanently.
70+
*/
4871
protected abstract doDispose(): void;
4972
}
5073

5174
/**
5275
* Used for all the IPC channels.
5376
*/
5477
export class ManagementConnection extends Connection {
55-
public constructor(protected protocol: Protocol, token: string) {
56-
super(protocol, token);
78+
public constructor(protocol: Protocol) {
79+
super(protocol, 'management');
5780
protocol.onDidDispose(() => this.dispose()); // Explicit close.
5881
protocol.onSocketClose(() => this.setOffline()); // Might reconnect.
82+
protocol.sendMessage({ type: 'ok' });
5983
}
6084

6185
protected doDispose(): void {
62-
this.protocol.sendDisconnect();
63-
this.protocol.dispose();
64-
this.protocol.getUnderlyingSocket().destroy();
86+
this.protocol.destroy();
6587
}
6688

67-
protected doReconnect(socket: ISocket, buffer: VSBuffer): void {
68-
this.protocol.beginAcceptReconnection(socket, buffer);
89+
protected doReconnect(protocol: Protocol): void {
90+
protocol.sendMessage({ type: 'ok' });
91+
this.protocol.beginAcceptReconnection(protocol.getSocket(), protocol.readEntireBuffer());
6992
this.protocol.endAcceptReconnection();
93+
protocol.dispose();
7094
}
7195
}
7296

@@ -85,55 +109,62 @@ type ExtHostMessage = DisconnectedMessage | ConsoleMessage | IExtHostReadyMessag
85109

86110
export class ExtensionHostConnection extends Connection {
87111
private process?: cp.ChildProcess;
88-
private readonly logger: Logger;
89112

90113
public constructor(
91-
locale: string, protocol: Protocol, buffer: VSBuffer, token: string,
114+
protocol: Protocol,
115+
private readonly params: IRemoteExtensionHostStartParams,
92116
private readonly environment: INativeEnvironmentService,
93117
) {
94-
super(protocol, token);
95-
this.logger = logger.named('exthost', field('token', token));
96-
this.protocol.dispose();
97-
this.spawn(locale, buffer).then((p) => this.process = p);
98-
this.protocol.getUnderlyingSocket().pause();
118+
super(protocol, 'exthost');
119+
120+
protocol.sendMessage({ debugPort: this.params.port });
121+
const buffer = protocol.readEntireBuffer();
122+
const inflateBytes = protocol.inflateBytes;
123+
protocol.dispose();
124+
protocol.getUnderlyingSocket().pause();
125+
126+
this.spawn(buffer, inflateBytes).then((p) => this.process = p);
99127
}
100128

101129
protected doDispose(): void {
130+
this.protocol.destroy();
102131
if (this.process) {
103132
this.process.kill();
104133
}
105-
this.protocol.getUnderlyingSocket().destroy();
106134
}
107135

108-
protected doReconnect(socket: ISocket, buffer: VSBuffer): void {
109-
// This is just to set the new socket.
110-
this.protocol.beginAcceptReconnection(socket, null);
111-
this.protocol.dispose();
112-
this.sendInitMessage(buffer);
136+
protected doReconnect(protocol: Protocol): void {
137+
protocol.sendMessage({ debugPort: this.params.port });
138+
const buffer = protocol.readEntireBuffer();
139+
const inflateBytes = protocol.inflateBytes;
140+
protocol.dispose();
141+
protocol.getUnderlyingSocket().pause();
142+
this.protocol.setSocket(protocol.getSocket());
143+
144+
this.sendInitMessage(buffer, inflateBytes);
113145
}
114146

115-
private sendInitMessage(buffer: VSBuffer): void {
116-
const socket = this.protocol.getUnderlyingSocket();
117-
socket.pause();
147+
private sendInitMessage(buffer: VSBuffer, inflateBytes: Uint8Array | undefined): void {
148+
if (!this.process) {
149+
throw new Error('Tried to initialize VS Code before spawning');
150+
}
118151

119-
const wrapperSocket = this.protocol.getSocket();
152+
this.logger.debug('Sending socket');
120153

121-
this.logger.trace('Sending socket');
122-
this.process!.send({ // Process must be set at this point.
154+
// TODO: Do something with the debug port.
155+
this.process.send({
123156
type: 'VSCODE_EXTHOST_IPC_SOCKET',
124157
initialDataChunk: Buffer.from(buffer.buffer).toString('base64'),
125-
skipWebSocketFrames: !(wrapperSocket instanceof WebSocketNodeSocket),
158+
skipWebSocketFrames: this.protocol.options.skipWebSocketFrames,
126159
permessageDeflate: this.protocol.options.permessageDeflate,
127-
inflateBytes: wrapperSocket instanceof WebSocketNodeSocket
128-
? Buffer.from(wrapperSocket.recordedInflateBytes.buffer).toString('base64')
129-
: undefined,
130-
}, socket);
160+
inflateBytes: inflateBytes ? Buffer.from(inflateBytes).toString('base64') : undefined,
161+
}, this.protocol.getUnderlyingSocket());
131162
}
132163

133-
private async spawn(locale: string, buffer: VSBuffer): Promise<cp.ChildProcess> {
134-
this.logger.trace('Getting NLS configuration...');
135-
const config = await getNlsConfiguration(locale, this.environment.userDataPath);
136-
this.logger.trace('Spawning extension host...');
164+
private async spawn(buffer: VSBuffer, inflateBytes: Uint8Array | undefined): Promise<cp.ChildProcess> {
165+
this.logger.debug('Getting NLS configuration...');
166+
const config = await getNlsConfiguration(this.params.language, this.environment.userDataPath);
167+
this.logger.debug('Spawning extension host...');
137168
const proc = cp.fork(
138169
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
139170
// While not technically necessary, makes it easier to tell which process
@@ -162,7 +193,7 @@ export class ExtensionHostConnection extends Connection {
162193
this.dispose();
163194
});
164195
proc.on('exit', (code) => {
165-
this.logger.trace('Exited', field('code', code));
196+
this.logger.debug('Exited', field('code', code));
166197
this.dispose();
167198
});
168199
if (proc.stdout && proc.stderr) {
@@ -181,20 +212,20 @@ export class ExtensionHostConnection extends Connection {
181212
}
182213
break;
183214
case 'VSCODE_EXTHOST_DISCONNECTED':
184-
this.logger.trace('Going offline');
215+
this.logger.debug('Got disconnected message');
185216
this.setOffline();
186217
break;
187218
case 'VSCODE_EXTHOST_IPC_READY':
188-
this.logger.trace('Got ready message');
189-
this.sendInitMessage(buffer);
219+
this.logger.debug('Handshake completed');
220+
this.sendInitMessage(buffer, inflateBytes);
190221
break;
191222
default:
192223
this.logger.error('Unexpected message', field('event', event));
193224
break;
194225
}
195226
});
196227

197-
this.logger.trace('Waiting for handshake...');
228+
this.logger.debug('Waiting for handshake...');
198229
return proc;
199230
}
200231
}

lib/vscode/src/vs/server/node/protocol.ts

+69-15
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
1-
import { field } from '@coder/logger';
1+
import { field, logger, Logger } from '@coder/logger';
22
import * as net from 'net';
33
import { VSBuffer } from 'vs/base/common/buffer';
44
import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
55
import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
66
import { AuthRequest, ConnectionTypeRequest, HandshakeMessage } from 'vs/platform/remote/common/remoteAgentConnection';
7-
import { logger } from 'vs/server/node/logger';
87

98
export interface SocketOptions {
9+
/** The token is how we identify and connect to existing sessions. */
1010
readonly reconnectionToken: string;
11+
/** Specifies that the client is trying to reconnect. */
1112
readonly reconnection: boolean;
13+
/** If true assume this is not a web socket (always false for code-server). */
1214
readonly skipWebSocketFrames: boolean;
15+
/** Whether to support compression (web socket only). */
1316
readonly permessageDeflate?: boolean;
17+
/**
18+
* Seed zlib with these bytes (web socket only). If parts of inflating was
19+
* done in a different zlib instance we need to pass all those bytes into zlib
20+
* otherwise the inflate might hit an inflated portion referencing a distance
21+
* too far back.
22+
*/
1423
readonly inflateBytes?: VSBuffer;
15-
readonly recordInflateBytes?: boolean;
1624
}
1725

1826
export class Protocol extends PersistentProtocol {
27+
private readonly logger: Logger;
28+
1929
public constructor(socket: net.Socket, public readonly options: SocketOptions) {
2030
super(
2131
options.skipWebSocketFrames
@@ -24,9 +34,12 @@ export class Protocol extends PersistentProtocol {
2434
new NodeSocket(socket),
2535
options.permessageDeflate || false,
2636
options.inflateBytes || null,
27-
options.recordInflateBytes || false,
37+
// Always record inflate bytes if using permessage-deflate.
38+
options.permessageDeflate || false,
2839
),
2940
);
41+
42+
this.logger = logger.named('protocol', field('token', this.options.reconnectionToken));
3043
}
3144

3245
public getUnderlyingSocket(): net.Socket {
@@ -40,31 +53,44 @@ export class Protocol extends PersistentProtocol {
4053
* Perform a handshake to get a connection request.
4154
*/
4255
public handshake(): Promise<ConnectionTypeRequest> {
43-
logger.trace('Protocol handshake', field('token', this.options.reconnectionToken));
56+
this.logger.debug('Initiating handshake...');
57+
4458
return new Promise((resolve, reject) => {
59+
const cleanup = () => {
60+
handler.dispose();
61+
onClose.dispose();
62+
clearTimeout(timeout);
63+
};
64+
65+
const onClose = this.onSocketClose(() => {
66+
cleanup();
67+
this.logger.debug('Handshake failed');
68+
reject(new Error('Protocol socket closed unexpectedly'));
69+
});
70+
4571
const timeout = setTimeout(() => {
46-
logger.error('Handshake timed out', field('token', this.options.reconnectionToken));
47-
reject(new Error('timed out'));
72+
cleanup();
73+
this.logger.debug('Handshake timed out');
74+
reject(new Error('Protocol handshake timed out'));
4875
}, 10000); // Matches the client timeout.
4976

5077
const handler = this.onControlMessage((rawMessage) => {
5178
try {
5279
const raw = rawMessage.toString();
53-
logger.trace('Protocol message', field('token', this.options.reconnectionToken), field('message', raw));
80+
this.logger.trace('Got message', field('message', raw));
5481
const message = JSON.parse(raw);
5582
switch (message.type) {
5683
case 'auth':
5784
return this.authenticate(message);
5885
case 'connectionType':
59-
handler.dispose();
60-
clearTimeout(timeout);
86+
cleanup();
87+
this.logger.debug('Handshake completed');
6188
return resolve(message);
6289
default:
6390
throw new Error('Unrecognized message type');
6491
}
6592
} catch (error) {
66-
handler.dispose();
67-
clearTimeout(timeout);
93+
cleanup();
6894
reject(error);
6995
}
7096
});
@@ -90,10 +116,38 @@ export class Protocol extends PersistentProtocol {
90116
}
91117

92118
/**
93-
* Send a handshake message. In the case of the extension host, it just sends
94-
* back a debug port.
119+
* Send a handshake message. In the case of the extension host it should just
120+
* send a debug port.
95121
*/
96-
public sendMessage(message: HandshakeMessage | { debugPort?: number } ): void {
122+
public sendMessage(message: HandshakeMessage | { debugPort?: number | null } ): void {
97123
this.sendControl(VSBuffer.fromString(JSON.stringify(message)));
98124
}
125+
126+
/**
127+
* Disconnect and dispose everything including the underlying socket.
128+
*/
129+
public destroy(reason?: string): void {
130+
try {
131+
if (reason) {
132+
this.sendMessage({ type: 'error', reason });
133+
}
134+
// If still connected try notifying the client.
135+
this.sendDisconnect();
136+
} catch (error) {
137+
// I think the write might fail if already disconnected.
138+
this.logger.warn(error.message || error);
139+
}
140+
this.dispose(); // This disposes timers and socket event handlers.
141+
this.getSocket().dispose(); // This will destroy() the socket.
142+
}
143+
144+
/**
145+
* Get inflateBytes from the current socket.
146+
*/
147+
public get inflateBytes(): Uint8Array | undefined {
148+
const socket = this.getSocket();
149+
return socket instanceof WebSocketNodeSocket
150+
? socket.recordedInflateBytes.buffer
151+
: undefined;
152+
}
99153
}

0 commit comments

Comments
 (0)