1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { inject, injectable, optional, postConstruct } from 'inversify';
|
18 | import { ILogger } from '../common/logger';
|
19 | import { Event, Emitter } from '../common/event';
|
20 | import { DefaultFrontendApplicationContribution } from './frontend-application';
|
21 | import { StatusBar, StatusBarAlignment } from './status-bar/status-bar';
|
22 | import { WebSocketConnectionProvider } from './messaging/ws-connection-provider';
|
23 | import { Disposable, DisposableCollection, nls } from '../common';
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | export const ConnectionStatusService = Symbol('ConnectionStatusService');
|
29 | export interface ConnectionStatusService {
|
30 |
|
31 | |
32 |
|
33 |
|
34 | readonly currentStatus: ConnectionStatus;
|
35 |
|
36 | |
37 |
|
38 |
|
39 | readonly onStatusChange: Event<ConnectionStatus>;
|
40 |
|
41 | }
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | export enum ConnectionStatus {
|
47 |
|
48 | |
49 |
|
50 |
|
51 | ONLINE,
|
52 |
|
53 | |
54 |
|
55 |
|
56 | OFFLINE
|
57 | }
|
58 |
|
59 | @injectable()
|
60 | export class ConnectionStatusOptions {
|
61 |
|
62 | static DEFAULT: ConnectionStatusOptions = {
|
63 | offlineTimeout: 5000,
|
64 | };
|
65 |
|
66 | |
67 |
|
68 |
|
69 | readonly offlineTimeout: number;
|
70 |
|
71 | }
|
72 |
|
73 | export const PingService = Symbol('PingService');
|
74 | export interface PingService {
|
75 | ping(): Promise<void>;
|
76 | }
|
77 |
|
78 | @injectable()
|
79 | export abstract class AbstractConnectionStatusService implements ConnectionStatusService, Disposable {
|
80 |
|
81 | protected readonly statusChangeEmitter = new Emitter<ConnectionStatus>();
|
82 |
|
83 | protected connectionStatus: ConnectionStatus = ConnectionStatus.ONLINE;
|
84 |
|
85 | @inject(ILogger)
|
86 | protected logger: ILogger;
|
87 |
|
88 | constructor(@inject(ConnectionStatusOptions) @optional() protected readonly options: ConnectionStatusOptions = ConnectionStatusOptions.DEFAULT) { }
|
89 |
|
90 | get onStatusChange(): Event<ConnectionStatus> {
|
91 | return this.statusChangeEmitter.event;
|
92 | }
|
93 |
|
94 | get currentStatus(): ConnectionStatus {
|
95 | return this.connectionStatus;
|
96 | }
|
97 |
|
98 | dispose(): void {
|
99 | this.statusChangeEmitter.dispose();
|
100 | }
|
101 |
|
102 | protected updateStatus(success: boolean): void {
|
103 | const previousStatus = this.connectionStatus;
|
104 | const newStatus = success ? ConnectionStatus.ONLINE : ConnectionStatus.OFFLINE;
|
105 | if (previousStatus !== newStatus) {
|
106 | this.connectionStatus = newStatus;
|
107 | this.fireStatusChange(newStatus);
|
108 | }
|
109 | }
|
110 |
|
111 | protected fireStatusChange(status: ConnectionStatus): void {
|
112 | this.statusChangeEmitter.fire(status);
|
113 | }
|
114 |
|
115 | }
|
116 |
|
117 | @injectable()
|
118 | export class FrontendConnectionStatusService extends AbstractConnectionStatusService {
|
119 |
|
120 | private scheduledPing: number | undefined;
|
121 |
|
122 | @inject(WebSocketConnectionProvider) protected readonly wsConnectionProvider: WebSocketConnectionProvider;
|
123 | @inject(PingService) protected readonly pingService: PingService;
|
124 |
|
125 | @postConstruct()
|
126 | protected init(): void {
|
127 | this.wsConnectionProvider.onSocketDidOpen(() => {
|
128 | this.updateStatus(true);
|
129 | this.schedulePing();
|
130 | });
|
131 | this.wsConnectionProvider.onSocketDidClose(() => {
|
132 | this.clearTimeout(this.scheduledPing);
|
133 | this.updateStatus(false);
|
134 | });
|
135 | this.wsConnectionProvider.onIncomingMessageActivity(() => {
|
136 |
|
137 | this.updateStatus(true);
|
138 | this.schedulePing();
|
139 | });
|
140 | }
|
141 |
|
142 | protected schedulePing(): void {
|
143 | this.clearTimeout(this.scheduledPing);
|
144 | this.scheduledPing = this.setTimeout(async () => {
|
145 | await this.performPingRequest();
|
146 | this.schedulePing();
|
147 | }, this.options.offlineTimeout);
|
148 | }
|
149 |
|
150 | protected async performPingRequest(): Promise<void> {
|
151 | try {
|
152 | await this.pingService.ping();
|
153 | this.updateStatus(true);
|
154 | } catch (e) {
|
155 | this.updateStatus(false);
|
156 | await this.logger.error(e);
|
157 | }
|
158 | }
|
159 |
|
160 |
|
161 | protected setTimeout(handler: (...args: any[]) => void, timeout: number): number {
|
162 | return window.setTimeout(handler, timeout);
|
163 | }
|
164 |
|
165 | protected clearTimeout(handle?: number): void {
|
166 | if (handle !== undefined) {
|
167 | window.clearTimeout(handle);
|
168 | }
|
169 | }
|
170 | }
|
171 |
|
172 | @injectable()
|
173 | export class ApplicationConnectionStatusContribution extends DefaultFrontendApplicationContribution {
|
174 |
|
175 | protected readonly toDisposeOnOnline = new DisposableCollection();
|
176 |
|
177 | constructor(
|
178 | @inject(ConnectionStatusService) protected readonly connectionStatusService: ConnectionStatusService,
|
179 | @inject(StatusBar) protected readonly statusBar: StatusBar,
|
180 | @inject(ILogger) protected readonly logger: ILogger
|
181 | ) {
|
182 | super();
|
183 | this.connectionStatusService.onStatusChange(state => this.onStateChange(state));
|
184 | }
|
185 |
|
186 | protected onStateChange(state: ConnectionStatus): void {
|
187 | switch (state) {
|
188 | case ConnectionStatus.OFFLINE: {
|
189 | this.handleOffline();
|
190 | break;
|
191 | }
|
192 | case ConnectionStatus.ONLINE: {
|
193 | this.handleOnline();
|
194 | break;
|
195 | }
|
196 | }
|
197 | }
|
198 |
|
199 | private statusbarId = 'connection-status';
|
200 |
|
201 | protected handleOnline(): void {
|
202 | this.toDisposeOnOnline.dispose();
|
203 | }
|
204 |
|
205 | protected handleOffline(): void {
|
206 | this.statusBar.setElement(this.statusbarId, {
|
207 | alignment: StatusBarAlignment.LEFT,
|
208 | text: nls.localize('theia/core/offline', 'Offline'),
|
209 | tooltip: nls.localize('theia/localize/offlineTooltip', 'Cannot connect to backend.'),
|
210 | priority: 5000
|
211 | });
|
212 | this.toDisposeOnOnline.push(Disposable.create(() => this.statusBar.removeElement(this.statusbarId)));
|
213 | document.body.classList.add('theia-mod-offline');
|
214 | this.toDisposeOnOnline.push(Disposable.create(() => document.body.classList.remove('theia-mod-offline')));
|
215 | }
|
216 | }
|