Skip to content

Commit eb95f33

Browse files
committed
Clean up extension host interfaces.
1 parent ac74ac1 commit eb95f33

File tree

4 files changed

+103
-23
lines changed

4 files changed

+103
-23
lines changed

src/vs/server/main.ts

+1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ export class ServerProcessMain extends Disposable implements IServerProcessMain
267267
// Web
268268
const webSocketServerService = new WebSocketServerService(
269269
this.netServer,
270+
productService,
270271
environmentServerService,
271272
logService,
272273
);

src/vs/server/connection/extensionHostConnection.ts renamed to src/vs/server/services/extensions/extensionHostConnection.ts

+91-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { SendHandle } from 'child_process';
22
import { VSBuffer } from 'vs/base/common/buffer';
3+
import { Emitter, Event } from 'vs/base/common/event';
34
import { FileAccess } from 'vs/base/common/network';
45
import { findFreePort } from 'vs/base/node/ports';
6+
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
7+
import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
58
import { IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp';
69
import { ILogService } from 'vs/platform/log/common/log';
710
import { DebugMessage, IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection';
811
import { AbstractConnection } from 'vs/server/connection/abstractConnection';
912
import { ServerProtocol } from 'vs/server/protocol';
1013
import { IEnvironmentServerService } from 'vs/server/services/environmentService';
1114
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
12-
import { ExtensionHost } from 'vs/workbench/services/extensions/node/extensionHost';
15+
import { createMessageOfType, MessageType } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
16+
import { ExtensionHostKind, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions';
17+
import { ExtensionHostProcess } from 'vs/workbench/services/extensions/node/extensionHost';
1318
import { getCachedNlsConfiguration } from 'vs/workbench/services/extensions/node/nls';
1419

1520
/**
@@ -32,9 +37,19 @@ export interface ForkEnvironmentVariables {
3237

3338
/**
3439
* This complements the client-side `PersistantConnection` in `RemoteExtensionHost`.
40+
* @see `LocalProcessExtensionHost`
3541
*/
36-
export class ExtensionHostConnection extends AbstractConnection {
37-
private clientProcess?: ExtensionHost;
42+
export class ExtensionHostConnection extends AbstractConnection implements IExtensionHost {
43+
public readonly kind = ExtensionHostKind.LocalProcess;
44+
public readonly remoteAuthority = null;
45+
public readonly lazyStart = false;
46+
private _terminating = false;
47+
48+
private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>();
49+
public readonly onExit: Event<[number, string]> = this._onExit.event;
50+
private _messageProtocol: Promise<PersistentProtocol> | null = null;
51+
52+
private clientProcess?: ExtensionHostProcess;
3853

3954
/** @TODO Document usage. */
4055
public readonly _isExtensionDevHost: boolean;
@@ -81,6 +96,16 @@ export class ExtensionHostConnection extends AbstractConnection {
8196
return port || 0;
8297
}
8398

99+
/** @TODO implement. */
100+
public getInspectPort(): number | undefined {
101+
return undefined;
102+
}
103+
104+
/** @TODO implement. */
105+
public enableInspectPort(): Promise<boolean> {
106+
return Promise.resolve(false);
107+
}
108+
84109
protected doDispose(): void {
85110
this.protocol.dispose();
86111

@@ -161,12 +186,27 @@ export class ExtensionHostConnection extends AbstractConnection {
161186
};
162187
}
163188

189+
private _onExtHostProcessExit(code: number, signal: string): void {
190+
this.dispose();
191+
192+
if (code !== 0 && signal !== 'SIGTERM') {
193+
this.logService.error(`${this.logPrefix} Extension host exited with code: ${code} and signal: ${signal}.`);
194+
}
195+
196+
if (this._terminating) {
197+
// Expected termination path (we asked the process to terminate)
198+
return;
199+
}
200+
201+
this._onExit.fire([code, signal]);
202+
}
203+
164204
/**
165205
* Creates an extension host child process.
166206
* @remark this is very similar to `LocalProcessExtensionHost`
167207
*/
168-
public spawn(): Promise<void> {
169-
return new Promise(async (resolve, reject) => {
208+
public start(): Promise<IMessagePassingProtocol> {
209+
this._messageProtocol = new Promise(async (resolve, reject) => {
170210
this.logService.debug(this.logPrefix, '(Spawn 1/7)', 'Sending client initial debug message.');
171211
this.protocol.sendMessage(this.debugMessage);
172212

@@ -178,14 +218,10 @@ export class ExtensionHostConnection extends AbstractConnection {
178218
const clientOptions = await this.generateClientOptions();
179219

180220
this.logService.debug(this.logPrefix, '(Spawn 4/7)', 'Starting extension host child process...');
181-
this.clientProcess = new ExtensionHost(FileAccess.asFileUri('bootstrap-fork', require).fsPath, clientOptions);
182-
183-
this.clientProcess.onDidProcessExit(({ code, signal }) => {
184-
this.dispose();
221+
this.clientProcess = new ExtensionHostProcess(FileAccess.asFileUri('bootstrap-fork', require).fsPath, clientOptions);
185222

186-
if (code !== 0 && signal !== 'SIGTERM') {
187-
this.logService.error(`${this.logPrefix} Extension host exited with code: ${code} and signal: ${signal}.`);
188-
}
223+
this.clientProcess.onDidProcessExit(([code, signal]) => {
224+
this._onExtHostProcessExit(code, signal);
189225
});
190226

191227
this.clientProcess.onReady(() => {
@@ -195,11 +231,53 @@ export class ExtensionHostConnection extends AbstractConnection {
195231

196232
if (messageSent) {
197233
this.logService.debug(this.logPrefix, '(Spawn 7/7)', 'Child process received init message!');
198-
return resolve();
234+
return resolve(this.protocol);
199235
}
200236

201237
reject(new Error('Child process did not receive init message. Is their a backlog?'));
202238
});
203239
});
240+
241+
return this._messageProtocol;
242+
}
243+
244+
public terminate(): void {
245+
if (this._terminating) {
246+
return;
247+
}
248+
this._terminating = true;
249+
250+
this.dispose();
251+
252+
if (!this._messageProtocol) {
253+
// .start() was not called
254+
return;
255+
}
256+
257+
this._messageProtocol.then((protocol) => {
258+
259+
// Send the extension host a request to terminate itself
260+
// (graceful termination)
261+
protocol.send(createMessageOfType(MessageType.Terminate));
262+
263+
protocol.getSocket().dispose();
264+
265+
protocol.dispose();
266+
267+
// Give the extension host 10s, after which we will
268+
// try to kill the process and release any resources
269+
setTimeout(() => this._cleanResources(), 10 * 1000);
270+
271+
}, (err) => {
272+
// Establishing a protocol with the extension host failed, so
273+
// try to kill the process and release any resources.
274+
this._cleanResources();
275+
});
276+
}
277+
278+
private _cleanResources(): void {
279+
if (this.clientProcess) {
280+
this.clientProcess.dispose();
281+
}
204282
}
205283
}

src/vs/server/services/net/webSocketServerService.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import { ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc';
1212
import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
1313
import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
1414
import { ILogService } from 'vs/platform/log/common/log';
15-
import product from 'vs/platform/product/common/product';
1615
import { ConnectionType, connectionTypeToString, IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection';
1716
import { ConnectionOptions, parseQueryConnectionOptions } from 'vs/server/connection/abstractConnection';
18-
import { ExtensionHostConnection } from 'vs/server/connection/extensionHostConnection';
17+
import { ExtensionHostConnection } from 'vs/server/services/extensions/extensionHostConnection';
1918
import { ManagementConnection } from 'vs/server/connection/managementConnection';
2019
import { ServerProtocol } from 'vs/server/protocol';
2120
import { AbstractIncomingRequestService, IAbstractIncomingRequestService, ParsedRequest } from 'vs/server/services/net/abstractIncomingRequestService';
2221
import { IEnvironmentServerService } from 'vs/server/services/environmentService';
22+
import { IProductService } from 'vs/platform/product/common/productService';
2323

2424
type Connection = ExtensionHostConnection | ManagementConnection;
2525

@@ -35,6 +35,8 @@ export const IWebSocketServerService = refineServiceDecorator<IAbstractIncomingR
3535
* Handles client connections to a editor instance via IPC.
3636
*/
3737
export class WebSocketServerService extends AbstractIncomingRequestService<UpgradeListener> implements IWebSocketServerService {
38+
productService: IProductService;
39+
3840
protected eventName = 'upgrade';
3941
private readonly _onDidClientConnect = new Emitter<ClientConnectionEvent>();
4042
public readonly onDidClientConnect = this._onDidClientConnect.event;
@@ -97,7 +99,7 @@ export class WebSocketServerService extends AbstractIncomingRequestService<Upgra
9799
const message = await protocol.handshake();
98100

99101
const clientVersion = message.commit;
100-
const serverVersion = product.commit;
102+
const serverVersion = this.productService.commit;
101103
if (serverVersion && clientVersion !== serverVersion) {
102104
this.logService.warn(`Client version (${message.commit} does not match server version ${serverVersion})`);
103105
}
@@ -153,8 +155,6 @@ export class WebSocketServerService extends AbstractIncomingRequestService<Upgra
153155
};
154156

155157
connection = new ExtensionHostConnection(protocol, this.logService, startParams, this.environmentService);
156-
157-
await connection.spawn();
158158
break;
159159
case ConnectionType.Tunnel:
160160
return protocol.tunnel();
@@ -178,10 +178,12 @@ export class WebSocketServerService extends AbstractIncomingRequestService<Upgra
178178

179179
constructor(
180180
netServer: net.Server,
181+
@IProductService productService: IProductService,
181182
@IEnvironmentServerService environmentService: IEnvironmentServerService,
182183
@ILogService logService: ILogService,
183184
) {
184185
super(netServer, environmentService, logService);
186+
this.productService = productService;
185187
}
186188
}
187189

src/vs/workbench/services/extensions/node/extensionHost.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export type ExtensionHostMessage = IRemoteConsoleLog | IExtHostReadyMessage;
1717
/**
1818
* Creates an extension host child process with a standard disposable interface.
1919
*/
20-
export class ExtensionHost implements IDisposable {
20+
export class ExtensionHostProcess implements IDisposable {
2121
private child: ChildProcess | null = null;
22-
private readonly _onDidProcessExit = new Emitter<{ code: number; signal: string }>();
22+
private readonly _onDidProcessExit = new Emitter<[number, string]>();
2323
public readonly onDidProcessExit = this._onDidProcessExit.event;
2424

2525
private readonly _onReadyMessage = new Emitter<IExtHostReadyMessage>();
@@ -102,14 +102,13 @@ export class ExtensionHost implements IDisposable {
102102

103103
this.child.on('error', err => console.warn('IPC "' + this.options.serverName + '" errored with ' + err));
104104

105-
this.child.on('exit', (code: any, signal: any) => {
105+
this.child.on('exit', (code: number, signal: string) => {
106106
process.removeListener('exit' as 'loaded', onExit); // https://github.com/electron/electron/issues/21475
107107

108108
if (code !== 0 && signal !== 'SIGTERM') {
109109
console.warn('IPC "' + this.options.serverName + '" crashed with exit code ' + code + ' and signal ' + signal);
110110
}
111-
112-
this._onDidProcessExit.fire({ code, signal });
111+
this._onDidProcessExit.fire([code, signal]);
113112
});
114113
}
115114
}

0 commit comments

Comments
 (0)