Skip to content

Commit 61f19b7

Browse files
committed
Fix issues where logger is defined before ready. Clean up extension lifecycle.
1 parent 03ffd24 commit 61f19b7

File tree

14 files changed

+361
-248
lines changed

14 files changed

+361
-248
lines changed

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@
8787
"vscode-ripgrep": "^1.12.0",
8888
"vscode-sqlite3": "4.0.11",
8989
"vscode-textmate": "5.4.0",
90-
"ws": "^8.0.0",
9190
"xterm": "4.13.0",
9291
"xterm-addon-search": "0.9.0-beta.3",
9392
"xterm-addon-unicode11": "0.3.0-beta.5",

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

+1-4
Original file line numberDiff line numberDiff line change
@@ -833,10 +833,7 @@ export class PersistentProtocol implements IMessagePassingProtocol {
833833
this._socketReader = new ProtocolReader(this._socket);
834834
this._socketDisposables.push(this._socketReader);
835835
this._socketDisposables.push(this._socketReader.onMessage(msg => this._receiveMessage(msg)));
836-
this._socketDisposables.push(this._socket.onClose((e) => {
837-
console.log('SOCKET DISPOSABLE CLOSE RECONNECTION', e?.type || 'unknown');
838-
return this._onSocketClose.fire(e);
839-
}));
836+
this._socketDisposables.push(this._socket.onClose((e) => this._onSocketClose.fire(e)));
840837
this._socketReader.acceptChunk(initialDataChunk);
841838
}
842839

src/vs/base/parts/ipc/node/ipc.cp.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ export class Client implements IChannelClient, IDisposable {
294294
}
295295

296296
export type ExtensionHostMessage = IRemoteConsoleLog | IExtHostReadyMessage;
297+
/**
298+
* Creates an extension host child process with a standard disposable interface.
299+
*/
297300
export class ExtensionHost implements IDisposable {
298301
private child: ChildProcess | null = null;
299302
private readonly _onDidProcessExit = new Emitter<{ code: number, signal: string }>();
@@ -315,7 +318,7 @@ export class ExtensionHost implements IDisposable {
315318
this.disposeClient();
316319
}
317320

318-
sendIPCMessage(message: IExtHostMessage, sendHandle?: SendHandle) {
321+
sendIPCMessage(message: IExtHostMessage, sendHandle?: SendHandle): boolean {
319322
if (this.child && this.child.connected) {
320323
return this.child.send(message, sendHandle);
321324
}
@@ -327,6 +330,7 @@ export class ExtensionHost implements IDisposable {
327330
const args = options && options.args ? this.options.args : [];
328331
const forkOpts: ForkOptions = Object.create(null);
329332

333+
forkOpts.silent = true;
330334
forkOpts.env = { ...deepClone(process.env), 'VSCODE_PARENT_PID': String(process.pid) };
331335

332336
if (this.options && this.options.env) {

src/vs/platform/remote/common/remoteAgentConnection.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ export const enum ConnectionType {
2525
Tunnel = 3,
2626
}
2727

28-
function connectionTypeToString(connectionType: ConnectionType): string {
28+
/**
29+
* @coder exported for server side use.
30+
*/
31+
export function connectionTypeToString(connectionType: ConnectionType): string {
2932
switch (connectionType) {
3033
case ConnectionType.Management:
3134
return 'Management';
@@ -238,12 +241,9 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio
238241
options.logService.info(`${logPrefix} 1/6. invoking socketFactory.connect().`);
239242

240243
let socket: ISocket;
244+
241245
try {
242-
/**
243-
* @coder Add connection type to the socket.
244-
* This is so they can be distinguished by the backend.
245-
*/
246-
socket = await createSocket(options.logService, options.socketFactory, options.host, options.port, `type=${connectionTypeToString(connectionType)}&reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, timeoutCancellationToken);
246+
socket = await createSocket(options.logService, options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, timeoutCancellationToken);
247247
} catch (error) {
248248
options.logService.error(`${logPrefix} socketFactory.connect() failed or timed out. Error:`);
249249
options.logService.error(error);

src/vs/server/connection/abstractConnection.ts

+43-2
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ export abstract class AbstractConnection {
1818
private disposed = false;
1919
private _offline: number | undefined;
2020

21-
protected readonly logService: ConsoleLogger;
21+
protected get logPrefix() {
22+
return `[${this.name}] ${this.protocol.logPrefix}`;
23+
}
2224

2325
public constructor(
2426
protected readonly protocol: ServerProtocol,
27+
protected readonly logService: ConsoleLogger,
2528
public readonly name: string,
2629
) {
27-
this.logService = new ConsoleLogger();
2830

2931
this.logService.debug('Connecting...');
3032
this.onClose(() => this.logService.debug('Closed'));
@@ -77,3 +79,42 @@ export abstract class AbstractConnection {
7779
*/
7880
protected abstract doDispose(): void;
7981
}
82+
83+
/**
84+
* Connection options sent by the client via a remote agent connection.
85+
*/
86+
export interface ConnectionOptions {
87+
/** The token is how we identify and connect to existing sessions. */
88+
readonly reconnectionToken: string,
89+
/** Specifies that the client is trying to reconnect. */
90+
readonly reconnection: boolean,
91+
/** If true assume this is not a web socket (always false for code-server). */
92+
readonly skipWebSocketFrames: boolean,
93+
}
94+
95+
/**
96+
* Convenience function to convert a client's query params into a useful object.
97+
*/
98+
export function parseQueryConnectionOptions(query: URLSearchParams): ConnectionOptions {
99+
const reconnectionToken = query.get('reconnectionToken');
100+
const reconnection = query.get('reconnection');
101+
const skipWebSocketFrames = query.get('skipWebSocketFrames');
102+
103+
if (typeof reconnectionToken !== 'string') {
104+
throw new Error('`reconnectionToken` not present in query');
105+
}
106+
107+
if (typeof reconnection !== 'string') {
108+
throw new Error('`reconnection` not present in query');
109+
}
110+
111+
if (typeof skipWebSocketFrames !== 'string') {
112+
throw new Error('`skipWebSocketFrames` not present in query');
113+
}
114+
115+
return {
116+
reconnectionToken,
117+
reconnection: reconnection === 'true',
118+
skipWebSocketFrames: skipWebSocketFrames === 'true',
119+
};
120+
}

src/vs/server/connection/extensionHostConnection.ts

+61-53
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/commo
99
import { findFreePort } from 'vs/base/node/ports';
1010
import { ExtensionHost, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp';
1111
import { VSBuffer } from 'vs/base/common/buffer';
12+
import { SendHandle } from 'child_process';
13+
import { ConsoleLogger } from 'vs/platform/log/common/log';
1214

1315
export interface ForkEnvironmentVariables {
1416
VSCODE_AMD_ENTRYPOINT: string;
@@ -24,26 +26,28 @@ export interface ForkEnvironmentVariables {
2426
VSCODE_CODE_CACHE_PATH?: string;
2527
}
2628

29+
/**
30+
* This complements the client-side `PersistantConnection` in `RemoteExtensionHost`.
31+
*/
2732
export class ExtensionHostConnection extends AbstractConnection {
2833
private clientProcess?: ExtensionHost;
2934

3035
/** @TODO Document usage. */
3136
public readonly _isExtensionDevHost: boolean;
3237
public readonly _isExtensionDevDebug: boolean;
33-
public readonly _isExtensionDevDebugBrk: boolean;
3438
public readonly _isExtensionDevTestFromCli: boolean;
3539

3640
public constructor(
3741
protocol: ServerProtocol,
42+
logService: ConsoleLogger,
3843
private readonly startParams: IRemoteExtensionHostStartParams,
3944
private readonly _environmentService: INativeEnvironmentService,
4045
) {
41-
super(protocol, 'exthost');
46+
super(protocol, logService, 'ExtensionHost');
4247

4348
const devOpts = parseExtensionDevOptions(this._environmentService);
4449
this._isExtensionDevHost = devOpts.isExtensionDevHost;
4550
this._isExtensionDevDebug = devOpts.isExtensionDevDebug;
46-
this._isExtensionDevDebugBrk = devOpts.isExtensionDevDebugBrk;
4751
this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli;
4852
}
4953

@@ -72,11 +76,6 @@ export class ExtensionHostConnection extends AbstractConnection {
7276
if (port !== expected) {
7377
console.warn(`%c[Extension Host] %cProvided debugging port ${expected} is not free, using ${port} instead.`, 'color: blue', 'color:');
7478
}
75-
if (this._isExtensionDevDebugBrk) {
76-
console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color:');
77-
} else {
78-
console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color:');
79-
}
8079
}
8180
}
8281

@@ -90,41 +89,44 @@ export class ExtensionHostConnection extends AbstractConnection {
9089
}
9190

9291
protected doReconnect(reconnectionProtocol: ServerProtocol): void {
92+
this.logService.info(this.logPrefix, '(Reconnect 1/4)', 'Sending new protocol debug message...');
93+
reconnectionProtocol.sendMessage(this.debugMessage);
94+
95+
this.logService.info(this.logPrefix, '(Reconnect 2/4)', 'Swapping socket references...');
96+
9397
this.protocol.beginAcceptReconnection(reconnectionProtocol.getSocket(), reconnectionProtocol.readEntireBuffer());
9498
this.protocol.endAcceptReconnection();
9599

96-
this.protocol.sendMessage(this.debugMessage);
100+
this.logService.info(this.logPrefix, '(Reconnect 3/4)', 'Pausing socket until we have a chance to forward its data.');
101+
const { initialDataChunk, sendHandle } = this.protocol.suspend();
97102

98-
const { initialDataChunk } = this.protocol;
99-
100-
// Pause reading on the socket until we have a chance to forward its data.
101-
this.protocol.dispose();
102-
this.protocol.getSendHandle().pause();
103-
this.protocol.getSocket().drain();
103+
const messageSent = this.sendInitMessage(initialDataChunk, this.protocol.inflateBytes, sendHandle);
104104

105+
if (!messageSent) {
106+
new Error('Child process did not receive init message. Is their a backlog?');
107+
}
105108

106-
this.sendInitMessage(initialDataChunk, this.protocol.inflateBytes);
109+
this.logService.info(this.logPrefix, '(Reconnect 4/4)', 'Child process received init message!');
107110
}
108111

109112
/**
110-
* Sends IPC socket to child process.
113+
* Sends IPC socket to client process.
114+
* @remark This is the complement of `extensionHostProcessSetup.ts#_createExtHostProtocol`
111115
*/
112-
private sendInitMessage(initialDataChunk: VSBuffer, inflateBytes?: VSBuffer): void {
113-
this.logService.info(this.logPrefix, 'Sending socket');
114-
115-
const foo = this.protocol.getSendHandle();
116-
117-
if (typeof foo === 'undefined') {
118-
throw new Error('SEND HANDLE IS UNDEFINED.');
116+
private sendInitMessage(initialDataChunk: VSBuffer, inflateBytes: VSBuffer, sendHandle: SendHandle): boolean {
117+
if (!this.clientProcess) {
118+
throw new Error(`${this.logPrefix} Client process is not set`);
119119
}
120120

121-
this.clientProcess?.sendIPCMessage({
121+
this.logService.info(this.logPrefix, 'Sending init message to client process...');
122+
123+
return this.clientProcess.sendIPCMessage({
122124
type: 'VSCODE_EXTHOST_IPC_SOCKET',
123125
initialDataChunk: Buffer.from(initialDataChunk.buffer).toString('base64'),
124126
skipWebSocketFrames: this.protocol.skipWebSocketFrames,
125127
permessageDeflate: this.protocol.getSocket().permessageDeflate,
126128
inflateBytes: inflateBytes ? Buffer.from(inflateBytes.buffer).toString('base64') : '',
127-
}, foo);
129+
}, sendHandle);
128130
}
129131

130132
private async generateClientOptions(): Promise<IIPCOptions> {
@@ -135,8 +137,8 @@ export class ExtensionHostConnection extends AbstractConnection {
135137
return {
136138
serverName: 'Server Extension Host',
137139
freshExecArgv: true,
138-
debugBrk: this._isExtensionDevDebugBrk ? portNumber : undefined,
139-
debug: this._isExtensionDevDebugBrk ? undefined : portNumber,
140+
debugBrk: this.startParams.break ? portNumber : undefined,
141+
debug: this.startParams.break ? undefined : portNumber,
140142
args: ['--type=extensionHost', '--skipWorkspaceStorageLock'],
141143
env: <ForkEnvironmentVariables>{
142144
VSCODE_AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess',
@@ -156,39 +158,45 @@ export class ExtensionHostConnection extends AbstractConnection {
156158
};
157159
}
158160

159-
public async spawn() {
160-
this.protocol.sendMessage(this.debugMessage);
161+
/**
162+
* Creates an extension host child process.
163+
* @remark this is very similar to `LocalProcessExtensionHost`
164+
*/
165+
public spawn(): Promise<void> {
166+
return new Promise(async (resolve, reject) => {
167+
this.logService.info(this.logPrefix, '(Spawn 1/7)', 'Sending client initial debug message.');
168+
this.protocol.sendMessage(this.debugMessage);
161169

162-
const { initialDataChunk } = this.protocol;
170+
this.logService.info(this.logPrefix, '(Spawn 2/7)', 'Pausing socket until we have a chance to forward its data.');
163171

164-
// Pause reading on the socket until we have a chance to forward its data.
165-
this.protocol.dispose();
166-
this.protocol.getSendHandle().pause();
167-
this.protocol.getSocket().drain();
172+
const { initialDataChunk, sendHandle } = this.protocol.suspend();
168173

169-
this.logService.debug(this.logPrefix, 'Spawning extension host...');
170-
const clientOptions = await this.generateClientOptions();
174+
this.logService.info(this.logPrefix, '(Spawn 3/7)', 'Generating IPC client options...');
175+
const clientOptions = await this.generateClientOptions();
171176

172-
// Run Extension Host as fork of current process
173-
this.clientProcess = new ExtensionHost(FileAccess.asFileUri('bootstrap-fork', require).fsPath, clientOptions);
177+
this.logService.info(this.logPrefix, '(Spawn 4/7)', 'Starting extension host child process...');
178+
this.clientProcess = new ExtensionHost(FileAccess.asFileUri('bootstrap-fork', require).fsPath, clientOptions);
174179

175-
this.clientProcess.onDidProcessExit(({ code, signal }) => {
176-
this.dispose();
180+
this.clientProcess.onDidProcessExit(({ code, signal }) => {
181+
this.dispose();
177182

178-
if (code !== 0 && signal !== 'SIGTERM') {
179-
this.logService.error(`[${this.protocol.reconnectionToken}] Extension host exited with code: ${code} and signal: ${signal}.`);
180-
}
181-
});
183+
if (code !== 0 && signal !== 'SIGTERM') {
184+
this.logService.error(`${this.logPrefix} Extension host exited with code: ${code} and signal: ${signal}.`);
185+
}
186+
});
182187

183-
this.clientProcess.onReady(() => {
184-
this.logService.info(this.logPrefix, 'Handshake completed');
185-
this.sendInitMessage(initialDataChunk, this.protocol.inflateBytes);
186-
});
188+
this.clientProcess.onReady(() => {
189+
this.logService.info(this.logPrefix, '(Spawn 5/7)', 'Extension host is ready!');
190+
this.logService.info(this.logPrefix, '(Spawn 6/7)', 'Sending init message to child process...');
191+
const messageSent = this.sendInitMessage(initialDataChunk, this.protocol.inflateBytes, sendHandle);
187192

188-
this.logService.info(this.logPrefix, 'Waiting for handshake...');
189-
}
193+
if (messageSent) {
194+
this.logService.info(this.logPrefix, '(Spawn 7/7)', 'Child process received init message!');
195+
return resolve();
196+
}
190197

191-
private get logPrefix() {
192-
return `[${this.protocol.reconnectionToken}]`;
198+
reject(new Error('Child process did not receive init message. Is their a backlog?'));
199+
});
200+
});
193201
}
194202
}

src/vs/server/connection/index.ts

-3
This file was deleted.

src/vs/server/connection/managementConnection.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
// * Licensed under the MIT License. See License.txt in the project root for license information.
44
// *--------------------------------------------------------------------------------------------*/
55

6+
import { ConsoleLogger } from 'vs/platform/log/common/log';
67
import { AbstractConnection } from 'vs/server/connection/abstractConnection';
78
import { ServerProtocol } from 'vs/server/protocol';
89

910
/**
1011
* Used for all the IPC channels.
1112
*/
1213
export class ManagementConnection extends AbstractConnection {
13-
public constructor(protocol: ServerProtocol) {
14-
super(protocol, 'management');
14+
public constructor(protocol: ServerProtocol, logService: ConsoleLogger) {
15+
super(protocol, logService, 'management');
1516

1617
protocol.onDidDispose(() => this.dispose('Explicitly closed'));
1718
protocol.onSocketClose(() => this.setOffline()); // Might reconnect.
@@ -23,10 +24,11 @@ export class ManagementConnection extends AbstractConnection {
2324
this.protocol.dispose();
2425
}
2526

26-
protected doReconnect(protocol: ServerProtocol): void {
27-
protocol.sendMessage({ type: 'ok' });
28-
this.protocol.beginAcceptReconnection(protocol.getSocket(), protocol.readEntireBuffer());
27+
protected doReconnect(reconnectionProtocol: ServerProtocol): void {
28+
reconnectionProtocol.sendMessage({ type: 'ok' });
29+
30+
this.protocol.beginAcceptReconnection(reconnectionProtocol.getSocket(), reconnectionProtocol.readEntireBuffer());
2931
this.protocol.endAcceptReconnection();
30-
protocol.dispose();
32+
reconnectionProtocol.dispose();
3133
}
3234
}

0 commit comments

Comments
 (0)