Skip to content

Commit 4e0a6d2

Browse files
committed
Partial extension host, some restructuring
I didn't like how the inner objects accessed parent objects, so I restructured all that.
1 parent 0d618bb commit 4e0a6d2

5 files changed

Lines changed: 354 additions & 223 deletions

File tree

connection.ts

Lines changed: 148 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,176 @@
1-
import { ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc";
2-
import { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection";
3-
import { Emitter } from "vs/base/common/event";
4-
import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net";
1+
import * as cp from "child_process";
2+
3+
import { getPathFromAmdModule } from "vs/base/common/amd";
54
import { VSBuffer } from "vs/base/common/buffer";
5+
import { Emitter } from "vs/base/common/event";
6+
import { ISocket } from "vs/base/parts/ipc/common/ipc.net";
7+
import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net";
8+
import { ILogService } from "vs/platform/log/common/log";
9+
import { IExtHostReadyMessage, IExtHostSocketMessage } from "vs/workbench/services/extensions/common/extensionHostProtocol";
610

7-
export interface Server {
8-
readonly _onDidClientConnect: Emitter<ClientConnectionEvent>;
9-
readonly connections: Map<ConnectionType, Map<string, Connection>>;
10-
}
11+
import { Protocol } from "vs/server/protocol";
1112

1213
export abstract class Connection {
1314
private readonly _onClose = new Emitter<void>();
1415
public readonly onClose = this._onClose.event;
1516

1617
private timeout: NodeJS.Timeout | undefined;
17-
private readonly wait = 1000 * 60 * 60;
18-
19-
public constructor(
20-
protected readonly server: Server,
21-
private readonly protocol: PersistentProtocol,
22-
) {
23-
// onClose seems to mean we want to disconnect, so dispose immediately.
24-
this.protocol.onClose(() => this.dispose());
25-
26-
// If the socket closes, we want to wait before disposing so we can
27-
// reconnect.
28-
this.protocol.onSocketClose(() => {
18+
private readonly wait = 1000 * 60;
19+
20+
private closed: boolean = false;
21+
22+
public constructor(protected protocol: Protocol) {
23+
// onClose seems to mean we want to disconnect, so close immediately.
24+
protocol.onClose(() => this.close());
25+
26+
// If the socket closes, we want to wait before closing so we can
27+
// reconnect in the meantime.
28+
protocol.onSocketClose(() => {
2929
this.timeout = setTimeout(() => {
30-
this.dispose();
30+
this.close();
3131
}, this.wait);
3232
});
3333
}
3434

3535
/**
36-
* Completely close and clean up the connection. Should only do this once we
37-
* don't need or want the connection. It cannot be re-used after this.
36+
* Set up the connection on a new socket.
3837
*/
39-
public dispose(): void {
40-
this.protocol.sendDisconnect();
41-
this.protocol.getSocket().end();
42-
this.protocol.dispose();
43-
this._onClose.fire();
38+
public reconnect(protocol: Protocol, buffer: VSBuffer): void {
39+
if (this.closed) {
40+
throw new Error("Cannot reconnect to closed connection");
41+
}
42+
clearTimeout(this.timeout as any); // Not sure why the type doesn't work.
43+
this.protocol = protocol;
44+
this.connect(protocol.getSocket(), buffer);
4445
}
4546

46-
public reconnect(socket: ISocket, buffer: VSBuffer): void {
47-
clearTimeout(this.timeout as any); // Not sure why the type doesn't work.
48-
this.protocol.beginAcceptReconnection(socket, buffer);
49-
this.protocol.endAcceptReconnection();
47+
/**
48+
* Close and clean up connection. This will also kill the socket the
49+
* connection is on. Probably not safe to reconnect once this has happened.
50+
*/
51+
protected close(): void {
52+
if (!this.closed) {
53+
this.closed = true;
54+
this.protocol.sendDisconnect();
55+
this.dispose();
56+
this.protocol.dispose();
57+
this._onClose.fire();
58+
}
5059
}
60+
61+
/**
62+
* Clean up the connection.
63+
*/
64+
protected abstract dispose(): void;
65+
66+
/**
67+
* Connect to a new socket.
68+
*/
69+
protected abstract connect(socket: ISocket, buffer: VSBuffer): void;
5170
}
5271

5372
/**
54-
* The management connection is used for all the IPC channels.
73+
* Used for all the IPC channels.
5574
*/
5675
export class ManagementConnection extends Connection {
57-
public constructor(server: Server, protocol: PersistentProtocol) {
58-
super(server, protocol);
59-
// This will communicate back to the IPCServer that a new client has
60-
// connected.
61-
this.server._onDidClientConnect.fire({
62-
protocol,
63-
onDidClientDisconnect: this.onClose,
64-
});
76+
protected dispose(): void {
77+
// Nothing extra to do here.
78+
}
79+
80+
protected connect(socket: ISocket, buffer: VSBuffer): void {
81+
this.protocol.beginAcceptReconnection(socket, buffer);
82+
this.protocol.endAcceptReconnection();
6583
}
6684
}
6785

86+
/**
87+
* Manage the extension host process.
88+
*/
6889
export class ExtensionHostConnection extends Connection {
90+
private process: cp.ChildProcess;
91+
92+
public constructor(protocol: Protocol, private readonly log: ILogService) {
93+
super(protocol);
94+
const socket = this.protocol.getSocket();
95+
const buffer = this.protocol.readEntireBuffer();
96+
this.process = this.spawn(socket, buffer);
97+
}
98+
99+
protected dispose(): void {
100+
this.process.kill();
101+
}
102+
103+
protected connect(socket: ISocket, buffer: VSBuffer): void {
104+
this.sendInitMessage(socket, buffer);
105+
}
106+
107+
private sendInitMessage(nodeSocket: ISocket, buffer: VSBuffer): void {
108+
const socket = nodeSocket instanceof NodeSocket
109+
? nodeSocket.socket
110+
: (nodeSocket as WebSocketNodeSocket).socket.socket;
111+
112+
socket.pause();
113+
114+
const initMessage: IExtHostSocketMessage = {
115+
type: "VSCODE_EXTHOST_IPC_SOCKET",
116+
initialDataChunk: (buffer.buffer as Buffer).toString("base64"),
117+
skipWebSocketFrames: nodeSocket instanceof NodeSocket,
118+
};
119+
120+
this.process.send(initMessage, socket);
121+
}
122+
123+
private spawn(socket: ISocket, buffer: VSBuffer): cp.ChildProcess {
124+
const proc = cp.fork(
125+
getPathFromAmdModule(require, "bootstrap-fork"),
126+
[
127+
"--type=extensionHost",
128+
`--uriTransformerPath=${getPathFromAmdModule(require, "vs/server/transformer")}`
129+
],
130+
{
131+
env: {
132+
...process.env,
133+
AMD_ENTRYPOINT: "vs/workbench/services/extensions/node/extensionHostProcess",
134+
PIPE_LOGGING: "true",
135+
VERBOSE_LOGGING: "true",
136+
VSCODE_EXTHOST_WILL_SEND_SOCKET: "true",
137+
VSCODE_HANDLES_UNCAUGHT_ERRORS: "true",
138+
VSCODE_LOG_STACK: "false",
139+
},
140+
silent: true,
141+
},
142+
);
143+
144+
proc.on("error", (error) => {
145+
console.error(error);
146+
this.close();
147+
});
148+
149+
proc.on("exit", (code, signal) => {
150+
console.error("Extension host exited", { code, signal });
151+
this.close();
152+
});
153+
154+
proc.stdout.setEncoding("utf8");
155+
proc.stderr.setEncoding("utf8");
156+
proc.stdout.on("data", (data) => this.log.info("Extension host stdout", data));
157+
proc.stderr.on("data", (data) => this.log.error("Extension host stderr", data));
158+
proc.on("message", (event) => {
159+
if (event && event.type === "__$console") {
160+
const severity = this.log[event.severity] ? event.severity : "info";
161+
this.log[severity]("Extension host", event.arguments);
162+
}
163+
});
164+
165+
const listen = (message: IExtHostReadyMessage) => {
166+
if (message.type === "VSCODE_EXTHOST_IPC_READY") {
167+
proc.removeListener("message", listen);
168+
this.sendInitMessage(socket, buffer);
169+
}
170+
};
171+
172+
proc.on("message", listen);
173+
174+
return proc;
175+
}
69176
}

protocol.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as crypto from "crypto";
2+
import * as net from "net";
3+
4+
import { VSBuffer } from "vs/base/common/buffer";
5+
import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net";
6+
import { PersistentProtocol } from "vs/base/parts/ipc/common/ipc.net";
7+
import { AuthRequest, ConnectionTypeRequest, HandshakeMessage } from "vs/platform/remote/common/remoteAgentConnection";
8+
9+
export interface SocketOptions {
10+
readonly reconnectionToken: string;
11+
readonly reconnection: boolean;
12+
readonly skipWebSocketFrames: boolean;
13+
}
14+
15+
export class Protocol extends PersistentProtocol {
16+
public constructor(
17+
secWebsocketKey: string,
18+
socket: net.Socket,
19+
public readonly options: SocketOptions,
20+
) {
21+
super(
22+
options.skipWebSocketFrames
23+
? new NodeSocket(socket)
24+
: new WebSocketNodeSocket(new NodeSocket(socket)),
25+
);
26+
socket.on("error", () => this.dispose());
27+
socket.on("end", () => this.dispose());
28+
29+
// This magic value is specified by the websocket spec.
30+
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
31+
const reply = crypto.createHash("sha1")
32+
.update(secWebsocketKey + magic)
33+
.digest("base64");
34+
35+
socket.write([
36+
"HTTP/1.1 101 Switching Protocols",
37+
"Upgrade: websocket",
38+
"Connection: Upgrade",
39+
`Sec-WebSocket-Accept: ${reply}`,
40+
].join("\r\n") + "\r\n\r\n");
41+
}
42+
43+
public dispose(error?: Error): void {
44+
if (error) {
45+
this.sendMessage({ type: "error", reason: error.message });
46+
}
47+
super.dispose();
48+
this.getSocket().dispose();
49+
}
50+
51+
/**
52+
* Perform a handshake to get a connection request.
53+
*/
54+
public handshake(): Promise<ConnectionTypeRequest> {
55+
return new Promise((resolve, reject) => {
56+
const handler = this.onControlMessage((rawMessage) => {
57+
try {
58+
const message = JSON.parse(rawMessage.toString());
59+
switch (message.type) {
60+
case "auth": return this.authenticate(message);
61+
case "connectionType":
62+
handler.dispose();
63+
return resolve(message);
64+
default: throw new Error("Unrecognized message type");
65+
}
66+
} catch (error) {
67+
handler.dispose();
68+
reject(error);
69+
}
70+
});
71+
});
72+
}
73+
74+
/**
75+
* TODO: This ignores the authentication process entirely for now.
76+
*/
77+
private authenticate(_message: AuthRequest): void {
78+
this.sendMessage({
79+
type: "sign",
80+
data: "",
81+
});
82+
}
83+
84+
/**
85+
* TODO: implement.
86+
*/
87+
public tunnel(): void {
88+
throw new Error("Tunnel is not implemented yet");
89+
}
90+
91+
/**
92+
* Send a handshake message. In the case of the extension host, it just sends
93+
* back a debug port.
94+
*/
95+
public sendMessage(message: HandshakeMessage | { debugPort?: number } ): void {
96+
this.sendControl(VSBuffer.fromString(JSON.stringify(message)));
97+
}
98+
}

0 commit comments

Comments
 (0)