From 9f4c03f97ded66421278fa94923d6f28391847a5 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Tue, 21 Jul 2020 15:32:07 +0300 Subject: [PATCH] add WebSocket failing warning Signed-off-by: Oleksii Orel --- src/assets/branding/product.json | 3 +- src/components/api/che-api-config.ts | 2 - src/components/api/che-websocket.factory.ts | 357 ------------------ .../api/json-rpc/che-json-rpc-api.factory.ts | 17 +- .../api/json-rpc/che-json-rpc-master-api.ts | 26 +- src/components/branding/branding.constant.ts | 2 + 6 files changed, 38 insertions(+), 369 deletions(-) delete mode 100644 src/components/api/che-websocket.factory.ts diff --git a/src/assets/branding/product.json b/src/assets/branding/product.json index 47180e97d..fbf245499 100644 --- a/src/assets/branding/product.json +++ b/src/assets/branding/product.json @@ -30,6 +30,7 @@ "converting": "https://www.eclipse.org/che/docs/che-7/converting-a-che-6-workspace-to-a-che-7-devfile/", "certificate": "https://www.eclipse.org/che/docs/che-7/installing-che-in-tls-mode-with-self-signed-certificates/#using-che-with-tls_installing-che-in-tls-mode-with-self-signed-certificates", "general": "https://www.eclipse.org/che/docs/che-7", - "storageTypes": "https://www.eclipse.org/che/docs/che-7/using-different-type-of-storage/" + "storageTypes": "https://www.eclipse.org/che/docs/che-7/using-different-type-of-storage/", + "webSocketTroubleshooting": "https://www.eclipse.org/che/docs/che-7/troubleshooting-network-problems/#troubleshooting%20websocket-secure-connections_troubleshooting-network-problems" } } diff --git a/src/components/api/che-api-config.ts b/src/components/api/che-api-config.ts index cd15e2135..b80ed08bd 100644 --- a/src/components/api/che-api-config.ts +++ b/src/components/api/che-api-config.ts @@ -14,7 +14,6 @@ import {CheAPI} from './che-api.factory'; import {CheWorkspace} from './workspace/che-workspace.factory'; import {CheFactory} from './che-factory.factory'; -import {CheWebsocket} from './che-websocket.factory'; import {CheProfile} from './che-profile.factory'; import {ChePreferences} from './che-preferences.factory'; import {CheService} from './che-service.factory'; @@ -49,7 +48,6 @@ export class ApiConfig { register.factory('cheFactory', CheFactory); register.factory('cheProfile', CheProfile); register.factory('chePreferences', ChePreferences); - register.factory('cheWebsocket', CheWebsocket); register.factory('cheHttpBackendProvider', CheHttpBackendProviderFactory); register.factory('cheHttpBackend', CheHttpBackendFactory); register.factory('cheAPIBuilder', CheAPIBuilder); diff --git a/src/components/api/che-websocket.factory.ts b/src/components/api/che-websocket.factory.ts deleted file mode 100644 index 5dcddb097..000000000 --- a/src/components/api/che-websocket.factory.ts +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Copyright (c) 2015-2018 Red Hat, Inc. - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - */ -'use strict'; - -/* exported MessageBus */ - -/** - * This class is handling the Websocket exchange - * @author Florent Benoit - */ -export class CheWebsocket { - - static $inject = ['$websocket', '$location', '$interval', 'proxySettings', 'userDashboardConfig', 'keycloakAuth']; - - private bus : MessageBus; - private wsBaseUrl : string; - private remoteBus : MessageBus; - private $interval : ng.IIntervalService; - private $websocket : any; - - /** - * Default constructor that is using resource - */ - constructor ($websocket: any, - $location: ng.ILocationService, - $interval: ng.IIntervalService, - proxySettings: string, - userDashboardConfig: any, - keycloakAuth: any) { - - this.$websocket = $websocket; - this.$interval = $interval; - - let wsUrl; - let inDevMode = userDashboardConfig.developmentMode; - - if (inDevMode) { - // it handle then http and https - wsUrl = proxySettings.replace('http', 'ws') + '/api/ws'; - } else { - - let wsProtocol; - if ('http' === $location.protocol()) { - wsProtocol = 'ws'; - } else { - wsProtocol = 'wss'; - } - - wsUrl = wsProtocol + '://' + $location.host() + ':' + $location.port() + '/api/ws'; - } - let keycloakToken = keycloakAuth.isPresent ? '?token=' + keycloakAuth.keycloak.token : ''; - this.wsBaseUrl = wsUrl + keycloakToken; - this.bus = null; - this.remoteBus = null; - } - - get wsUrl(): string { - return this.wsBaseUrl; - } - - getExistingBus(datastream: any): MessageBus { - return new MessageBus(datastream, this.$interval); - } - - getBus() : MessageBus { - if (!this.bus) { - // needs to initialize - this.bus = new MessageBus(this.$websocket(this.wsBaseUrl), this.$interval); - this.bus.onClose( - () => { - // remove it from the cache so new calls will create a new instance - this.bus.closed = true; - this.bus = null; - } - ); - } - return this.bus; - } - - /** - * Gets a bus for a remote workspace, by providing the remote URL to this websocket - * @param {string} websocketURL the remote host base WS url - */ - getRemoteBus(websocketURL: string): MessageBus { - if (!this.remoteBus) { - // needs to initialize - this.remoteBus = new MessageBus(this.$websocket(websocketURL), this.$interval); - this.remoteBus.onClose( - () => { - // remove it from the cache so new calls will create a new instance - this.remoteBus.closed = true; - this.remoteBus = null; - } - ); - } - return this.remoteBus; - } - -} - - -class MessageBuilder { - - private static TYPE : string = 'x-everrest-websocket-message-type'; - private method : string; - private path : string; - private message : any; - - constructor(method? : string, path? : string) { - if (method) { - this.method = method; - } else { - this.method = 'POST'; - } - if (path) { - this.path = path; - } else { - this.path = null; - } - - - this.message = {}; - // add uuid - this.message.uuid = this.buildUUID(); - - this.message.method = this.method; - this.message.path = this.path; - this.message.headers = []; - this.message.body = ''; - } - - subscribe(channel: any): MessageBuilder { - let header = {name: MessageBuilder.TYPE, value: 'subscribe-channel'}; - this.message.headers.push(header); - this.message.body = JSON.stringify({channel: channel}); - return this; - } - - unsubscribe(channel: any): MessageBuilder { - let header = {name: MessageBuilder.TYPE, value: 'unsubscribe-channel'}; - this.message.headers.push(header); - this.message.body = JSON.stringify({channel: channel}); - return this; - } - - /** - * Prepares ping frame for server. - * - * @returns {MessageBuilder} - */ - ping(): MessageBuilder { - let header = {name: MessageBuilder.TYPE, value: 'ping'}; - this.message.headers.push(header); - return this; - } - - build(): any { - return this.message; - } - - buildUUID(): string { - /* tslint:disable */ - let time = new Date().getTime(); - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (match: string) => { - let rem = (time + 16 * Math.random()) % 16 | 0; // jshint ignore:line - time = Math.floor(time / 16); - return (match === 'x' ? rem : rem & 7 | 8).toString(16); // jshint ignore:line - }); - /* tslint:enable */ - } - -} - -export class MessageBus { - - closed: boolean; - datastream: any; - private $interval: ng.IIntervalService; - private heartbeatPeriod: number; - private subscribersByChannel: Map; - private keepAlive : ng.IPromise; - - constructor(datastream: any, - $interval: ng.IIntervalService) { - this.datastream = datastream; - this.$interval = $interval; - - this.heartbeatPeriod = 1000 * 50; // ping each 50 seconds - - this.subscribersByChannel = new Map(); - - this.setKeepAlive(); - this.datastream.onMessage((message: any) => { this.handleMessage(message); }); - } - - public isClosed() : boolean { - return this.closed; - } - - /** - * Sets the keep alive interval, which sends - * ping frame to server to keep connection alive. - */ - setKeepAlive(): void { - this.keepAlive = this.$interval(() => { - this.ping(); - }, this.heartbeatPeriod); - } - - /** - * Sends ping frame to server. - */ - ping(): void { - this.send(new MessageBuilder().ping().build()); - } - - /** - * Handles websocket closed event. - * - * @param callback - */ - onClose(callback: Function): void { - this.datastream.onClose(callback); - } - - /** - * Handles websocket error event. - * - * @param callback - */ - onError(callback: Function): void { - this.datastream.onError(callback); - } - - /** - * Restart ping timer (cancel previous and start again). - */ - restartPing(): void { - if (this.keepAlive) { - this.$interval.cancel(this.keepAlive); - } - - this.setKeepAlive(); - } - - /** - * Subscribes a new callback which will listener for messages sent to the specified channel. - * Upon the first subscribe to a channel, a message is sent to the server to - * subscribe the client for that channel. Subsequent subscribes for a channel - * already previously subscribed to do not trigger a send of another message - * to the server because the client has already a subscription, and merely registers - * (client side) the additional handler to be fired for events received on the respective channel. - */ - subscribe(channel: any, callback: Function): void { - // already subscribed ? - let existingSubscribers = this.subscribersByChannel.get(channel); - if (!existingSubscribers) { - // register callback - - let subscribers = []; - subscribers.push(callback); - this.subscribersByChannel.set(channel, subscribers); - - // send subscribe order - this.send(new MessageBuilder().subscribe(channel).build()); - } else { - // existing there, add only callback - existingSubscribers.push(callback); - } - } - - - - /** - * Unsubscribes a previously subscribed handler listening on the specified channel. - * If it's the last unsubscribe to a channel, a message is sent to the server to - * unsubscribe the client for that channel. - */ - unsubscribe(channel: any): void { - // already subscribed ? - let existingSubscribers = this.subscribersByChannel.get(channel); - // unable to cancel if not existing channel - if (!existingSubscribers) { - return; - } - - if (existingSubscribers > 1) { - // only remove callback - for (let i = 0; i < existingSubscribers.length; i++) { - delete existingSubscribers[i]; - } - } else { - // only one element, remove and send server message - this.subscribersByChannel.delete(channel); - - // send unsubscribe order - this.send(new MessageBuilder().unsubscribe(channel).build()); - } - } - - send(message: any): void { - let stringified = JSON.stringify(message); - this.datastream.send(stringified); - } - - handleMessage(message: any): void { - // handling the receive of a message - // needs to parse it - let jsonMessage = JSON.parse(message.data); - - // get headers - let headers = jsonMessage.headers; - - - let channelHeader; - // found channel headers - for (let i = 0; i < headers.length; i++) { - let header = headers[i]; - if ('x-everrest-websocket-channel' === header.name) { - channelHeader = header; - } - } - - // handle case when we don't have channel but a raw message - if (!channelHeader && headers.length === 1 && headers[0].name === 'x-everrest-websocket-message-type') { - channelHeader = headers[0]; - } - - if (channelHeader) { - // message for a channel, look at current subscribers - let subscribers = this.subscribersByChannel.get(channelHeader.value); - if (subscribers) { - subscribers.forEach((subscriber: any) => { - try { - subscriber(angular.fromJson(jsonMessage.body)); - } catch (e) { - subscriber(jsonMessage.body); - } - }); - } - } - - // restart ping after received message - this.restartPing(); - } - -} - diff --git a/src/components/api/json-rpc/che-json-rpc-api.factory.ts b/src/components/api/json-rpc/che-json-rpc-api.factory.ts index 9ecbe6451..1bfc1f017 100644 --- a/src/components/api/json-rpc/che-json-rpc-api.factory.ts +++ b/src/components/api/json-rpc/che-json-rpc-api.factory.ts @@ -15,6 +15,7 @@ import {CheJsonRpcMasterApi} from './che-json-rpc-master-api'; import {WebsocketClient} from './websocket-client'; import {CheJsonRpcWsagentApi} from './che-json-rpc-wsagent-api'; import {CheKeycloak} from '../che-keycloak.factory'; +import { GlobalWarningBannerService } from '../../service/global-warning-banner.service'; /** * This class manages the api connection through JSON RPC. @@ -23,7 +24,7 @@ import {CheKeycloak} from '../che-keycloak.factory'; */ export class CheJsonRpcApi { - static $inject = ['$q', '$websocket', '$log', '$timeout', '$interval', 'cheKeycloak']; + static $inject = ['$q', '$websocket', '$log', '$timeout', '$interval', 'globalWarningBannerService', 'cheKeycloak']; private $q: ng.IQService; private $websocket: any; @@ -31,6 +32,7 @@ export class CheJsonRpcApi { private jsonRpcApiConnection: Map; private $timeout: ng.ITimeoutService; private $interval: ng.IIntervalService; + private globalWarningBannerService: GlobalWarningBannerService; private cheKeycloak: CheKeycloak; /** @@ -42,6 +44,7 @@ export class CheJsonRpcApi { $log: ng.ILogService, $timeout: ng.ITimeoutService, $interval: ng.IIntervalService, + globalWarningBannerService: GlobalWarningBannerService, cheKeycloak: CheKeycloak ) { this.$q = $q; @@ -50,6 +53,7 @@ export class CheJsonRpcApi { this.$timeout = $timeout; this.$interval = $interval; this.jsonRpcApiConnection = new Map(); + this.globalWarningBannerService = globalWarningBannerService; this.cheKeycloak = cheKeycloak; } @@ -58,7 +62,16 @@ export class CheJsonRpcApi { return this.jsonRpcApiConnection.get(entrypoint); } else { const websocketClient = new WebsocketClient(this.$websocket, this.$q); - const cheJsonRpcMasterApi: CheJsonRpcMasterApi = new CheJsonRpcMasterApi(websocketClient, entrypoint, this.$log, this.$timeout, this.$interval, this.$q, this.cheKeycloak); + const cheJsonRpcMasterApi: CheJsonRpcMasterApi = new CheJsonRpcMasterApi( + websocketClient, + entrypoint, + this.$log, + this.$timeout, + this.$interval, + this.$q, + this.globalWarningBannerService, + this.cheKeycloak + ); this.jsonRpcApiConnection.set(entrypoint, cheJsonRpcMasterApi); return cheJsonRpcMasterApi; } diff --git a/src/components/api/json-rpc/che-json-rpc-master-api.ts b/src/components/api/json-rpc/che-json-rpc-master-api.ts index d03b85c5d..4dcdc69bf 100644 --- a/src/components/api/json-rpc/che-json-rpc-master-api.ts +++ b/src/components/api/json-rpc/che-json-rpc-master-api.ts @@ -13,6 +13,8 @@ import {CheJsonRpcApiClient} from './che-json-rpc-api-service'; import {ICommunicationClient} from './json-rpc-client'; import {CheKeycloak} from '../che-keycloak.factory'; +import { GlobalWarningBannerService } from '../../service/global-warning-banner.service'; +import { CheBranding } from '../../../components/branding/che-branding'; enum MasterChannels { ENVIRONMENT_OUTPUT = 'runtime/log', @@ -42,16 +44,18 @@ export class CheJsonRpcMasterApi { private $timeout: ng.ITimeoutService; private $interval: ng.IIntervalService; private $q: ng.IQService; + private globalWarningBannerService: GlobalWarningBannerService; private cheKeycloak: CheKeycloak; private cheJsonRpcApi: CheJsonRpcApiClient; private clientId: string; private checkingInterval: ng.IPromise; private reconnectionAttemptTimeout: ng.IPromise; + private branding: CheBranding; - private maxReconnectionAttempts = 100; + private maxReconnectionAttempts = 5; private reconnectionAttemptNumber = 0; - private reconnectionDelay = 30000; - private checkingDelay = 10000; + private reconnectionDelay = 10000; + private checkingDelay = 5000; private fetchingClientIdTimeout = 5000; constructor( @@ -61,18 +65,22 @@ export class CheJsonRpcMasterApi { $timeout: ng.ITimeoutService, $interval: ng.IIntervalService, $q: ng.IQService, + globalWarningBannerService: GlobalWarningBannerService, cheKeycloak: CheKeycloak ) { this.$log = $log; this.$timeout = $timeout; this.$interval = $interval; this.$q = $q; + this.globalWarningBannerService = globalWarningBannerService; this.cheKeycloak = cheKeycloak; client.addListener('open', () => this.onConnectionOpen(entrypoint)); client.addListener('close', () => this.onConnectionClose(entrypoint)); this.cheJsonRpcApi = new CheJsonRpcApiClient(client); + this.branding = CheBranding.get(); + this.connect(entrypoint); } @@ -126,8 +134,13 @@ export class CheJsonRpcMasterApi { } reconnect(entrypoint: string): void { - this.$log.warn('WebSocket connection is closed.'); - if (this.reconnectionAttemptNumber === this.maxReconnectionAttempts) { + if (this.reconnectionAttemptNumber < this.maxReconnectionAttempts) { + this.$log.warn('WebSocket connection is closed.'); + } else { + this.globalWarningBannerService.addMessage(`WebSocket connections "${entrypoint}" are failing due to network restrictions. + ${this.branding.getName()} workspaces may not be usable. Please refer to the + Network Troubleshooting + section of the ${this.branding.getName()} User Guide.`); this.$log.warn('The maximum number of attempts to reconnect WebSocket has been reached.'); if (this.checkingInterval) { @@ -215,7 +228,6 @@ export class CheJsonRpcMasterApi { * Un-subscribes the pointed callback from the environment output. * * @param workspaceId workspace's id - * @param machineName machine's name * @param callback callback to process event */ unSubscribeEnvironmentOutput(workspaceId: string, callback: Function): void { @@ -330,7 +342,7 @@ export class CheJsonRpcMasterApi { /** * Fetch client's id and strores it. * - * @returns {IPromise} + * @returns {IPromise} */ fetchClientId(): ng.IPromise { return this.cheJsonRpcApi.request('websocketIdService/getId').then((data: any) => { diff --git a/src/components/branding/branding.constant.ts b/src/components/branding/branding.constant.ts index d90a5ba8b..4109f4ffd 100644 --- a/src/components/branding/branding.constant.ts +++ b/src/components/branding/branding.constant.ts @@ -44,6 +44,7 @@ export type IBrandingDocs = { certificate: string, faq?: string, storageTypes: string, + webSocketTroubleshooting: string, } export type IBrandingWorkspace = { @@ -109,6 +110,7 @@ export const BRANDING_DEFAULT: IBranding = { certificate: 'https://www.eclipse.org/che/docs/che-7/installing-che-in-tls-mode-with-self-signed-certificates/#using-che-with-tls_installing-che-in-tls-mode-with-self-signed-certificates', general: 'https://www.eclipse.org/che/docs/che-7', storageTypes: "https://www.eclipse.org/che/docs/che-7/using-different-type-of-storage/", + webSocketTroubleshooting: 'https://www.eclipse.org/che/docs/che-7/troubleshooting-network-problems/#troubleshooting%20websocket-secure-connections_troubleshooting-network-problems', }, configuration: { menu: {