Skip to content

Commit 1b44c0f

Browse files
authored
core: refactor communication layer to use socket.io (#10514)
1 parent 535208f commit 1b44c0f

15 files changed

+254
-400
lines changed

CHANGELOG.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
- [plugin-ext] function `logMeasurement` of `PluginDeployerImpl` class and browser class `HostedPluginSupport` is replaced by `measure` using the new `Stopwatch` API [#10407](https://github.com/eclipse-theia/theia/pull/10407)
1414
- [plugin-ext] the constructor of `BackendApplication` class no longer invokes the `initialize` method. Instead, the `@postConstruct configure` method now starts by calling `initialize` [#10407](https://github.com/eclipse-theia/theia/pull/10407)
1515
- [plugin] Added support for `vscode.window.createStatusBarItem` [#10754](https://github.com/eclipse-theia/theia/pull/10754) - Contributed on behalf of STMicroelectronics
16+
- [core] Replaced raw WebSocket transport with Socket.io protocol, changed internal APIs accordingly
17+
- [core] Removed all of our own custom HTTP Polling implementation
1618

1719
## v1.22.0 - 1/27/2022
1820

@@ -89,7 +91,7 @@
8991
- [plugin] added support for codicon icon references in view containers [#10491](https://github.com/eclipse-theia/theia/pull/10491)
9092
- [plugin] added support to set theme attributes in webviews [#10493](https://github.com/eclipse-theia/theia/pull/10493)
9193
- [plugin] fixed running plugin hosts on `electron` for `Windows` [#10518](https://github.com/eclipse-theia/theia/pull/10518)
92-
- [preferences] updated `AbstractResourcePreferenceProvider` to handle multiple preference settings in the same tick and handle open preference files.
94+
- [preferences] updated `AbstractResourcePreferenceProvider` to handle multiple preference settings in the same tick and handle open preference files.
9395
It will save the file exactly once, and prompt the user if the file is dirty when a programmatic setting is attempted. [#7775](https://github.com/eclipse-theia/theia/pull/7775)
9496
- [preferences] added support for non-string enum values in schemas [#10511](https://github.com/eclipse-theia/theia/pull/10511)
9597
- [preferences] added support for rendering markdown descriptions [#10431](https://github.com/eclipse-theia/theia/pull/10431)
@@ -113,11 +115,11 @@
113115
- [plugin] changed return type of `WebviewThemeDataProvider.getActiveTheme()` to `Theme` instead of `WebviewThemeType` [#10493](https://github.com/eclipse-theia/theia/pull/10493)
114116
- [plugin] removed the application prop `resolveSystemPlugins`, builtin plugins should now be resolved at build time [#10353](https://github.com/eclipse-theia/theia/pull/10353)
115117
- [plugin] renamed `WebviewThemeData.activeTheme` to `activeThemeType` [#10493](https://github.com/eclipse-theia/theia/pull/10493)
116-
- [preferences] removed `PreferenceProvider#pendingChanges` field. It was previously set unreliably and caused race conditions.
117-
If a `PreferenceProvider` needs a mechanism for deferring the resolution of `PreferenceProvider#setPreference`, it should implement its own system.
118+
- [preferences] removed `PreferenceProvider#pendingChanges` field. It was previously set unreliably and caused race conditions.
119+
If a `PreferenceProvider` needs a mechanism for deferring the resolution of `PreferenceProvider#setPreference`, it should implement its own system.
118120
See PR for example implementation in `AbstractResourcePreferenceProvider`. [#7775](https://github.com/eclipse-theia/theia/pull/7775)
119121
- [terminal] removed deprecated `activateTerminal` method in favor of `open`. [#10529](https://github.com/eclipse-theia/theia/pull/10529)
120-
- [webpack] Source maps for the frontend renamed from `webpack://[namespace]/[resource-filename]...` to `webpack:///[resource-path]?[loaders]` where `resource-path` is the path to
122+
- [webpack] Source maps for the frontend renamed from `webpack://[namespace]/[resource-filename]...` to `webpack:///[resource-path]?[loaders]` where `resource-path` is the path to
121123
the file relative to your application package's root [#10480](https://github.com/eclipse-theia/theia/pull/10480)
122124

123125
## v1.20.0 - 11/25/2021

doc/Migration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ For example:
1919
}
2020
```
2121

22+
### v1.24.0
23+
24+
#### From WebSocket to Socket.io
25+
26+
This is a very important change to how Theia sends and receives messages with its backend.
27+
28+
This new Socket.io protocol will try to establish a WebSocket connection whenever possible, but it may also
29+
setup HTTP polling. It may even try to connect through HTTP before attempting WebSocket.
30+
31+
Make sure your network configurations support both WebSockets and/or HTTP polling.
32+
2233
### v1.23.0
2334

2435
#### TypeScript 4.5.5

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
"reflect-metadata": "^0.1.10",
6464
"route-parser": "^0.0.5",
6565
"safer-buffer": "^2.1.2",
66+
"socket.io": "4.1.0",
67+
"socket.io-client": "4.1.0",
6668
"uuid": "^8.3.2",
6769
"vscode-languageserver-protocol": "~3.15.3",
6870
"vscode-uri": "^2.1.1",

packages/core/src/browser/frontend-application-bindings.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
********************************************************************************/
1616

1717
import { interfaces } from 'inversify';
18-
import { bindContributionProvider, DefaultResourceProvider, MessageClient, MessageService, MessageServiceFactory, ResourceProvider, ResourceResolver } from '../common';
18+
import { bindContributionProvider, DefaultResourceProvider, MessageClient, MessageService, ResourceProvider, ResourceResolver } from '../common';
1919
import {
2020
bindPreferenceSchemaProvider, PreferenceProvider,
2121
PreferenceProviderProvider, PreferenceSchemaProvider, PreferenceScope,
@@ -24,7 +24,6 @@ import {
2424

2525
export function bindMessageService(bind: interfaces.Bind): interfaces.BindingWhenOnSyntax<MessageService> {
2626
bind(MessageClient).toSelf().inSingletonScope();
27-
bind(MessageServiceFactory).toFactory(({ container }) => () => container.get(MessageService));
2827
return bind(MessageService).toSelf().inSingletonScope();
2928
}
3029

packages/core/src/browser/messaging/messaging-frontend-module.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
********************************************************************************/
1616

1717
import { ContainerModule } from 'inversify';
18-
import { DEFAULT_HTTP_FALLBACK_OPTIONS, HttpFallbackOptions, WebSocketConnectionProvider } from './ws-connection-provider';
18+
import { WebSocketConnectionProvider } from './ws-connection-provider';
1919

2020
export const messagingFrontendModule = new ContainerModule(bind => {
21-
bind(HttpFallbackOptions).toConstantValue(DEFAULT_HTTP_FALLBACK_OPTIONS);
2221
bind(WebSocketConnectionProvider).toSelf().inSingletonScope();
2322
});

packages/core/src/browser/messaging/ws-connection-provider.ts

Lines changed: 27 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
import { injectable, interfaces, decorate, unmanaged, inject, optional } from 'inversify';
18-
import { JsonRpcProxyFactory, JsonRpcProxy, Emitter, Event, MessageService, MessageServiceFactory } from '../../common';
17+
import { injectable, interfaces, decorate, unmanaged } from 'inversify';
18+
import { JsonRpcProxyFactory, JsonRpcProxy, Emitter, Event } from '../../common';
1919
import { WebSocketChannel } from '../../common/messaging/web-socket-channel';
2020
import { Endpoint } from '../endpoint';
21-
import ReconnectingWebSocket from 'reconnecting-websocket';
2221
import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider';
23-
import { v4 as uuid } from 'uuid';
22+
import { io, Socket } from 'socket.io-client';
2423

2524
decorate(injectable(), JsonRpcProxyFactory);
2625
decorate(unmanaged(), JsonRpcProxyFactory, 0);
@@ -32,29 +31,6 @@ export interface WebSocketOptions {
3231
reconnecting?: boolean;
3332
}
3433

35-
export const HttpFallbackOptions = Symbol('HttpFallbackOptions');
36-
37-
export interface HttpFallbackOptions {
38-
/** Determines whether Theia is allowed to use the http fallback. True by default. */
39-
allowed: boolean;
40-
/** Number of failed websocket connection attempts before the fallback is triggered. 2 by default. */
41-
maxAttempts: number;
42-
/** The maximum duration (in ms) after which the http request should timeout. 5000 by default. */
43-
pollingTimeout: number;
44-
/** The timeout duration (in ms) after a request was answered with an error code. 5000 by default. */
45-
errorTimeout: number;
46-
/** The minimum timeout duration (in ms) between two http requests. 0 by default. */
47-
requestTimeout: number;
48-
}
49-
50-
export const DEFAULT_HTTP_FALLBACK_OPTIONS: HttpFallbackOptions = {
51-
allowed: true,
52-
maxAttempts: 2,
53-
errorTimeout: 5000,
54-
pollingTimeout: 5000,
55-
requestTimeout: 0
56-
};
57-
5834
@injectable()
5935
export class WebSocketConnectionProvider extends AbstractConnectionProvider<WebSocketOptions> {
6036

@@ -68,128 +44,47 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider<WebS
6844
return this.onSocketDidCloseEmitter.event;
6945
}
7046

71-
protected readonly onHttpFallbackDidActivateEmitter: Emitter<void> = new Emitter();
72-
get onHttpFallbackDidActivate(): Event<void> {
73-
return this.onHttpFallbackDidActivateEmitter.event;
74-
}
75-
7647
static createProxy<T extends object>(container: interfaces.Container, path: string, arg?: object): JsonRpcProxy<T> {
7748
return container.get(WebSocketConnectionProvider).createProxy<T>(path, arg);
7849
}
7950

80-
@inject(MessageServiceFactory)
81-
protected readonly messageService: () => MessageService;
82-
83-
@inject(HttpFallbackOptions) @optional()
84-
protected readonly httpFallbackOptions: HttpFallbackOptions | undefined;
85-
86-
protected readonly socket: ReconnectingWebSocket;
87-
protected useHttpFallback = false;
88-
protected websocketErrorCounter = 0;
89-
protected httpFallbackId = uuid();
90-
protected httpFallbackDisconnected = true;
51+
protected readonly socket: Socket;
9152

9253
constructor() {
9354
super();
9455
const url = this.createWebSocketUrl(WebSocketChannel.wsPath);
9556
const socket = this.createWebSocket(url);
96-
socket.onerror = event => this.handleSocketError(event);
97-
socket.onopen = () => {
57+
socket.on('connect', () => {
9858
this.fireSocketDidOpen();
99-
};
100-
socket.onclose = ({ code, reason }) => {
59+
});
60+
socket.on('disconnect', reason => {
10161
for (const channel of [...this.channels.values()]) {
102-
channel.close(code, reason);
62+
channel.close(undefined, reason);
10363
}
10464
this.fireSocketDidClose();
105-
};
106-
socket.onmessage = ({ data }) => {
65+
});
66+
socket.on('message', data => {
10767
this.handleIncomingRawMessage(data);
108-
};
68+
});
69+
socket.connect();
10970
this.socket = socket;
110-
window.addEventListener('offline', () => this.tryReconnect());
111-
window.addEventListener('online', () => this.tryReconnect());
112-
}
113-
114-
handleSocketError(event: unknown): void {
115-
this.websocketErrorCounter += 1;
116-
if (this.httpFallbackOptions?.allowed && this.websocketErrorCounter >= this.httpFallbackOptions?.maxAttempts) {
117-
this.useHttpFallback = true;
118-
this.socket.close();
119-
const httpUrl = this.createHttpWebSocketUrl(WebSocketChannel.wsPath);
120-
this.onHttpFallbackDidActivateEmitter.fire(undefined);
121-
this.doLongPolling(httpUrl);
122-
this.messageService().warn(
123-
'Could not establish a websocket connection. The application will be using the HTTP fallback mode. This may affect performance and the behavior of some features.'
124-
);
125-
}
126-
console.error(event);
127-
}
128-
129-
async doLongPolling(url: string): Promise<void> {
130-
let timeoutDuration = this.httpFallbackOptions?.requestTimeout || 0;
131-
const controller = new AbortController();
132-
const pollingId = window.setTimeout(() => controller.abort(), this.httpFallbackOptions?.pollingTimeout);
133-
try {
134-
const response = await fetch(url, {
135-
method: 'POST',
136-
headers: {
137-
'Content-Type': 'application/json'
138-
},
139-
signal: controller.signal,
140-
keepalive: true,
141-
body: JSON.stringify({ id: this.httpFallbackId, polling: true })
142-
});
143-
if (response.status === 200) {
144-
window.clearTimeout(pollingId);
145-
if (this.httpFallbackDisconnected) {
146-
this.fireSocketDidOpen();
147-
}
148-
const json: string[] = await response.json();
149-
if (Array.isArray(json)) {
150-
for (const item of json) {
151-
this.handleIncomingRawMessage(item);
152-
}
153-
} else {
154-
throw new Error('Received invalid long polling response.');
155-
}
156-
} else {
157-
timeoutDuration = this.httpFallbackOptions?.errorTimeout || 0;
158-
this.httpFallbackDisconnected = true;
159-
this.fireSocketDidClose();
160-
throw new Error('Response has error code: ' + response.status);
161-
}
162-
} catch (e) {
163-
console.error('Error occurred during long polling', e);
164-
}
165-
setTimeout(() => this.doLongPolling(url), timeoutDuration);
16671
}
16772

16873
openChannel(path: string, handler: (channel: WebSocketChannel) => void, options?: WebSocketOptions): void {
169-
if (this.useHttpFallback || this.socket.readyState === WebSocket.OPEN) {
74+
if (this.socket.connected) {
17075
super.openChannel(path, handler, options);
17176
} else {
17277
const openChannel = () => {
173-
this.socket.removeEventListener('open', openChannel);
78+
this.socket.off('connect', openChannel);
17479
this.openChannel(path, handler, options);
17580
};
176-
this.socket.addEventListener('open', openChannel);
177-
this.onHttpFallbackDidActivate(openChannel);
81+
this.socket.on('connect', openChannel);
17882
}
17983
}
18084

18185
protected createChannel(id: number): WebSocketChannel {
182-
const httpUrl = this.createHttpWebSocketUrl(WebSocketChannel.wsPath);
18386
return new WebSocketChannel(id, content => {
184-
if (this.useHttpFallback) {
185-
fetch(httpUrl, {
186-
method: 'POST',
187-
headers: {
188-
'Content-Type': 'application/json'
189-
},
190-
body: JSON.stringify({ id: this.httpFallbackId, content })
191-
});
192-
} else if (this.socket.readyState < WebSocket.CLOSING) {
87+
if (this.socket.connected) {
19388
this.socket.send(content);
19489
}
19590
});
@@ -211,33 +106,25 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider<WebS
211106
/**
212107
* Creates a web socket for the given url
213108
*/
214-
protected createWebSocket(url: string): ReconnectingWebSocket {
215-
return new ReconnectingWebSocket(url, undefined, {
216-
maxReconnectionDelay: 10000,
217-
minReconnectionDelay: 1000,
218-
reconnectionDelayGrowFactor: 1.3,
219-
connectionTimeout: 10000,
220-
maxRetries: Infinity,
221-
debug: false
109+
protected createWebSocket(url: string): Socket {
110+
return io(url, {
111+
reconnection: true,
112+
reconnectionDelay: 1000,
113+
reconnectionDelayMax: 10000,
114+
reconnectionAttempts: Infinity,
115+
extraHeaders: {
116+
// Socket.io strips the `origin` header
117+
// We need to provide our own for validation
118+
'fix-origin': window.location.origin
119+
}
222120
});
223121
}
224122

225123
protected fireSocketDidOpen(): void {
226-
// Once a websocket connection has opened, disable the http fallback
227-
if (this.httpFallbackOptions?.allowed) {
228-
this.httpFallbackOptions.allowed = false;
229-
}
230124
this.onSocketDidOpenEmitter.fire(undefined);
231125
}
232126

233127
protected fireSocketDidClose(): void {
234128
this.onSocketDidCloseEmitter.fire(undefined);
235129
}
236-
237-
protected tryReconnect(): void {
238-
if (!this.useHttpFallback && this.socket.readyState !== WebSocket.CONNECTING) {
239-
this.socket.reconnect();
240-
}
241-
}
242-
243130
}

packages/core/src/common/message-service.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ import {
2525
} from './message-service-protocol';
2626
import { CancellationTokenSource } from './cancellation';
2727

28-
export const MessageServiceFactory = Symbol('MessageServiceFactory');
29-
3028
/**
3129
* Service to log and categorize messages, show progress information and offer actions.
3230
*

packages/core/src/electron-node/token/electron-token-messaging-contribution.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
import * as net from 'net';
1817
import * as http from 'http';
1918
import { injectable, inject } from 'inversify';
2019
import { MessagingContribution } from '../../node/messaging/messaging-contribution';
@@ -33,13 +32,10 @@ export class ElectronMessagingContribution extends MessagingContribution {
3332
/**
3433
* Only allow token-bearers.
3534
*/
36-
protected handleHttpUpgrade(request: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
35+
protected async allowConnect(request: http.IncomingMessage): Promise<boolean> {
3736
if (this.tokenValidator.allowRequest(request)) {
38-
super.handleHttpUpgrade(request, socket, head);
39-
} else {
40-
console.error(`refused a websocket connection: ${request.connection.remoteAddress}`);
41-
socket.destroy(); // kill connection, client will take that as a "no".
37+
return super.allowConnect(request);
4238
}
39+
return false;
4340
}
44-
4541
}

0 commit comments

Comments
 (0)