From 4e0de7ce575505947c9c406edc8b0a46d009d952 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Sep 2019 21:48:46 +0200 Subject: [PATCH] web connection --- src/vs/base/browser/dom.ts | 12 +- .../remote/browser/browserSocketFactory.ts | 82 +++++++- .../contrib/remote/browser/remote.ts | 195 ++++++++++++++++++ .../electron-browser/remote.contribution.ts | 192 +---------------- 4 files changed, 280 insertions(+), 201 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 9887331522f..d9d424f84e4 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -203,11 +203,11 @@ export const toggleClass: (node: HTMLElement | SVGElement, className: string, sh class DomListener implements IDisposable { private _handler: (e: any) => void; - private _node: Element | Window | Document; + private _node: EventTarget; private readonly _type: string; private readonly _options: boolean | AddEventListenerOptions; - constructor(node: Element | Window | Document, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) { + constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) { this._node = node; this._type = type; this._handler = handler; @@ -229,10 +229,10 @@ class DomListener implements IDisposable { } } -export function addDisposableListener(node: Element | Window | Document, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable; -export function addDisposableListener(node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable; -export function addDisposableListener(node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture: AddEventListenerOptions): IDisposable; -export function addDisposableListener(node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture?: boolean | AddEventListenerOptions): IDisposable { +export function addDisposableListener(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable; +export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable; +export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture: AddEventListenerOptions): IDisposable; +export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean | AddEventListenerOptions): IDisposable { return new DomListener(node, type, handler, useCapture); } diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index 6b24ec07810..d0f6e6b18a6 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -8,6 +8,9 @@ import { ISocket } from 'vs/base/parts/ipc/common/ipc.net'; import { VSBuffer } from 'vs/base/common/buffer'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; +import * as dom from 'vs/base/browser/dom'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; export interface IWebSocketFactory { create(url: string): IWebSocket; @@ -23,27 +26,34 @@ export interface IWebSocket { close(): void; } -class BrowserWebSocket implements IWebSocket { +class BrowserWebSocket extends Disposable implements IWebSocket { private readonly _onData = new Emitter(); public readonly onData = this._onData.event; public readonly onOpen: Event; - public readonly onClose: Event; - public readonly onError: Event; + + private readonly _onClose = this._register(new Emitter()); + public readonly onClose = this._onClose.event; + + private readonly _onError = this._register(new Emitter()); + public readonly onError = this._onError.event; private readonly _socket: WebSocket; private readonly _fileReader: FileReader; private readonly _queue: Blob[]; private _isReading: boolean; + private _isClosed: boolean; private readonly _socketMessageListener: (ev: MessageEvent) => void; constructor(socket: WebSocket) { + super(); this._socket = socket; this._fileReader = new FileReader(); this._queue = []; this._isReading = false; + this._isClosed = false; this._fileReader.onload = (event) => { this._isReading = false; @@ -71,17 +81,79 @@ class BrowserWebSocket implements IWebSocket { this._socket.addEventListener('message', this._socketMessageListener); this.onOpen = Event.fromDOMEventEmitter(this._socket, 'open'); - this.onClose = Event.fromDOMEventEmitter(this._socket, 'close'); - this.onError = Event.fromDOMEventEmitter(this._socket, 'error'); + + // WebSockets emit error events that do not contain any real information + // Our only chance of getting to the root cause of an error is to + // listen to the close event which gives out some real information: + // - https://www.w3.org/TR/websockets/#closeevent + // - https://tools.ietf.org/html/rfc6455#section-11.7 + // + // But the error event is emitted before the close event, so we therefore + // delay the error event processing in the hope of receiving a close event + // with more information + + let pendingErrorEvent: any | null = null; + + const sendPendingErrorNow = () => { + const err = pendingErrorEvent; + pendingErrorEvent = null; + this._onError.fire(err); + }; + + const errorRunner = this._register(new RunOnceScheduler(sendPendingErrorNow, 0)); + + const sendErrorSoon = (err: any) => { + errorRunner.cancel(); + pendingErrorEvent = err; + errorRunner.schedule(); + }; + + const sendErrorNow = (err: any) => { + errorRunner.cancel(); + pendingErrorEvent = err; + sendPendingErrorNow(); + }; + + this._register(dom.addDisposableListener(this._socket, 'close', (e: CloseEvent) => { + this._isClosed = true; + + if (pendingErrorEvent) { + if (!window.navigator.onLine) { + // The browser is offline => this is a temporary error which might resolve itself + sendErrorNow(new RemoteAuthorityResolverError('Browser is offline', RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable, e)); + } else { + // An error event is pending + // The browser appears to be online... + if (!e.wasClean) { + // Let's be optimistic and hope that perhaps the server could not be reached or something + sendErrorNow(new RemoteAuthorityResolverError(e.reason || `WebSocket close with status code ${e.code}`, RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable, e)); + } else { + // this was a clean close => send existing error + errorRunner.cancel(); + sendPendingErrorNow(); + } + } + } + + this._onClose.fire(); + })); + + this._register(dom.addDisposableListener(this._socket, 'error', sendErrorSoon)); } send(data: ArrayBuffer | ArrayBufferView): void { + if (this._isClosed) { + // Refuse to write data to closed WebSocket... + return; + } this._socket.send(data); } close(): void { + this._isClosed = true; this._socket.close(); this._socket.removeEventListener('message', this._socketMessageListener); + this.dispose(); } } diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index df1df4650af..1abc5d22b22 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -38,6 +38,15 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { IProgress, IProgressStep, IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ReconnectionWaitEvent, PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; +import Severity from 'vs/base/common/severity'; +import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; interface HelpInformation { extensionDescription: IExtensionDescription; @@ -445,3 +454,189 @@ Registry.as(WorkbenchActionExtensions.WorkbenchActions 'View: Show Remote Explorer', nls.localize('view', "View") ); + + +class ProgressReporter { + private _currentProgress: IProgress | null = null; + private lastReport: string | null = null; + + constructor(currentProgress: IProgress | null) { + this._currentProgress = currentProgress; + } + + set currentProgress(progress: IProgress) { + this._currentProgress = progress; + } + + report(message?: string) { + if (message) { + this.lastReport = message; + } + + if (this.lastReport && this._currentProgress) { + this._currentProgress.report({ message: this.lastReport }); + } + } +} + +class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { + constructor( + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IProgressService progressService: IProgressService, + @IDialogService dialogService: IDialogService, + @ICommandService commandService: ICommandService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + const connection = remoteAgentService.getConnection(); + if (connection) { + let currentProgressPromiseResolve: (() => void) | null = null; + let progressReporter: ProgressReporter | null = null; + let lastLocation: ProgressLocation | null = null; + let currentTimer: ReconnectionTimer | null = null; + let reconnectWaitEvent: ReconnectionWaitEvent | null = null; + let disposableListener: IDisposable | null = null; + + function showProgress(location: ProgressLocation, buttons?: string[]) { + if (currentProgressPromiseResolve) { + currentProgressPromiseResolve(); + } + + const promise = new Promise((resolve) => currentProgressPromiseResolve = resolve); + lastLocation = location; + + if (location === ProgressLocation.Dialog) { + // Show dialog + progressService!.withProgress( + { location: ProgressLocation.Dialog, buttons }, + (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, + (choice?) => { + // Handle choice from dialog + if (choice === 0 && buttons && reconnectWaitEvent) { + reconnectWaitEvent.skipWait(); + } else { + showProgress(ProgressLocation.Notification, buttons); + } + + progressReporter!.report(); + }); + } else { + // Show notification + progressService!.withProgress( + { location: ProgressLocation.Notification, buttons }, + (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, + (choice?) => { + // Handle choice from notification + if (choice === 0 && buttons && reconnectWaitEvent) { + reconnectWaitEvent.skipWait(); + } else { + hideProgress(); + } + }); + } + } + + function hideProgress() { + if (currentProgressPromiseResolve) { + currentProgressPromiseResolve(); + } + + currentProgressPromiseResolve = null; + } + + connection.onDidStateChange((e) => { + if (currentTimer) { + currentTimer.dispose(); + currentTimer = null; + } + + if (disposableListener) { + disposableListener.dispose(); + disposableListener = null; + } + switch (e.type) { + case PersistentConnectionEventType.ConnectionLost: + if (!currentProgressPromiseResolve) { + progressReporter = new ProgressReporter(null); + showProgress(ProgressLocation.Dialog, [nls.localize('reconnectNow', "Reconnect Now")]); + } + + progressReporter!.report(nls.localize('connectionLost', "Connection Lost")); + break; + case PersistentConnectionEventType.ReconnectionWait: + hideProgress(); + reconnectWaitEvent = e; + showProgress(lastLocation || ProgressLocation.Notification, [nls.localize('reconnectNow', "Reconnect Now")]); + currentTimer = new ReconnectionTimer(progressReporter!, Date.now() + 1000 * e.durationSeconds); + break; + case PersistentConnectionEventType.ReconnectionRunning: + hideProgress(); + showProgress(lastLocation || ProgressLocation.Notification); + progressReporter!.report(nls.localize('reconnectionRunning', "Attempting to reconnect...")); + + // Register to listen for quick input is opened + disposableListener = contextKeyService.onDidChangeContext((contextKeyChangeEvent) => { + const reconnectInteraction = new Set(['inQuickOpen']); + if (contextKeyChangeEvent.affectsSome(reconnectInteraction)) { + // Need to move from dialog if being shown and user needs to type in a prompt + if (lastLocation === ProgressLocation.Dialog && progressReporter !== null) { + hideProgress(); + showProgress(ProgressLocation.Notification); + progressReporter.report(); + } + } + }); + + break; + case PersistentConnectionEventType.ReconnectionPermanentFailure: + hideProgress(); + progressReporter = null; + + dialogService.show(Severity.Error, nls.localize('reconnectionPermanentFailure', "Cannot reconnect. Please reload the window."), [nls.localize('reloadWindow', "Reload Window"), nls.localize('cancel', "Cancel")], { cancelId: 1 }).then(result => { + // Reload the window + if (result.choice === 0) { + commandService.executeCommand(ReloadWindowAction.ID); + } + }); + break; + case PersistentConnectionEventType.ConnectionGain: + hideProgress(); + progressReporter = null; + break; + } + }); + } + } +} + +class ReconnectionTimer implements IDisposable { + private readonly _progressReporter: ProgressReporter; + private readonly _completionTime: number; + private readonly _token: any; + + constructor(progressReporter: ProgressReporter, completionTime: number) { + this._progressReporter = progressReporter; + this._completionTime = completionTime; + this._token = setInterval(() => this._render(), 1000); + this._render(); + } + + public dispose(): void { + clearInterval(this._token); + } + + private _render() { + const remainingTimeMs = this._completionTime - Date.now(); + if (remainingTimeMs < 0) { + return; + } + const remainingTime = Math.ceil(remainingTimeMs / 1000); + if (remainingTime === 1) { + this._progressReporter.report(nls.localize('reconnectionWaitOne', "Attempting to reconnect in {0} second...", remainingTime)); + } else { + this._progressReporter.report(nls.localize('reconnectionWaitMany', "Attempting to reconnect in {0} seconds...", remainingTime)); + } + } +} + +const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts index 816e4b5bef6..185c5a1d085 100644 --- a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts @@ -6,13 +6,11 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme'; - import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; - import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { MenuId, IMenuService, MenuItemAction, IMenu, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; @@ -32,14 +30,11 @@ import { LogLevelSetterChannel } from 'vs/platform/log/common/logIpc'; import { ipcRenderer as ipc } from 'electron'; import { IDiagnosticInfoOptions, IRemoteDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IProgressService, IProgress, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { PersistentConnectionEventType, ReconnectionWaitEvent } from 'vs/platform/remote/common/remoteAgentConnection'; +import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import Severity from 'vs/base/common/severity'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { OpenFileFolderAction, OpenLocalFileFolderCommand, OpenFileAction, OpenFolderAction, OpenLocalFileCommand, OpenLocalFolderCommand, SaveLocalFileCommand } from 'vs/workbench/browser/actions/workspaceActions'; -import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { RemoteConnectionState, Deprecated_RemoteAuthorityContext, RemoteFileDialogContext } from 'vs/workbench/browser/contextkeys'; @@ -266,29 +261,6 @@ class RemoteAgentDiagnosticListener implements IWorkbenchContribution { } } -class ProgressReporter { - private _currentProgress: IProgress | null = null; - private lastReport: string | null = null; - - constructor(currentProgress: IProgress | null) { - this._currentProgress = currentProgress; - } - - set currentProgress(progress: IProgress) { - this._currentProgress = progress; - } - - report(message?: string) { - if (message) { - this.lastReport = message; - } - - if (this.lastReport && this._currentProgress) { - this._currentProgress.report({ message: this.lastReport }); - } - } -} - class RemoteExtensionHostEnvironmentUpdater implements IWorkbenchContribution { constructor( @IRemoteAgentService remoteAgentService: IRemoteAgentService, @@ -309,165 +281,6 @@ class RemoteExtensionHostEnvironmentUpdater implements IWorkbenchContribution { } } -class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { - constructor( - @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IProgressService progressService: IProgressService, - @IDialogService dialogService: IDialogService, - @ICommandService commandService: ICommandService, - @IContextKeyService contextKeyService: IContextKeyService - ) { - const connection = remoteAgentService.getConnection(); - if (connection) { - let currentProgressPromiseResolve: (() => void) | null = null; - let progressReporter: ProgressReporter | null = null; - let lastLocation: ProgressLocation | null = null; - let currentTimer: ReconnectionTimer | null = null; - let reconnectWaitEvent: ReconnectionWaitEvent | null = null; - let disposableListener: IDisposable | null = null; - - function showProgress(location: ProgressLocation, buttons?: string[]) { - if (currentProgressPromiseResolve) { - currentProgressPromiseResolve(); - } - - const promise = new Promise((resolve) => currentProgressPromiseResolve = resolve); - lastLocation = location; - - if (location === ProgressLocation.Dialog) { - // Show dialog - progressService!.withProgress( - { location: ProgressLocation.Dialog, buttons }, - (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, - (choice?) => { - // Handle choice from dialog - if (choice === 0 && buttons && reconnectWaitEvent) { - reconnectWaitEvent.skipWait(); - } else { - showProgress(ProgressLocation.Notification, buttons); - } - - progressReporter!.report(); - }); - } else { - // Show notification - progressService!.withProgress( - { location: ProgressLocation.Notification, buttons }, - (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, - (choice?) => { - // Handle choice from notification - if (choice === 0 && buttons && reconnectWaitEvent) { - reconnectWaitEvent.skipWait(); - } else { - hideProgress(); - } - }); - } - } - - function hideProgress() { - if (currentProgressPromiseResolve) { - currentProgressPromiseResolve(); - } - - currentProgressPromiseResolve = null; - } - - connection.onDidStateChange((e) => { - if (currentTimer) { - currentTimer.dispose(); - currentTimer = null; - } - - if (disposableListener) { - disposableListener.dispose(); - disposableListener = null; - } - switch (e.type) { - case PersistentConnectionEventType.ConnectionLost: - if (!currentProgressPromiseResolve) { - progressReporter = new ProgressReporter(null); - showProgress(ProgressLocation.Dialog, [nls.localize('reconnectNow', "Reconnect Now")]); - } - - progressReporter!.report(nls.localize('connectionLost', "Connection Lost")); - break; - case PersistentConnectionEventType.ReconnectionWait: - hideProgress(); - reconnectWaitEvent = e; - showProgress(lastLocation || ProgressLocation.Notification, [nls.localize('reconnectNow', "Reconnect Now")]); - currentTimer = new ReconnectionTimer(progressReporter!, Date.now() + 1000 * e.durationSeconds); - break; - case PersistentConnectionEventType.ReconnectionRunning: - hideProgress(); - showProgress(lastLocation || ProgressLocation.Notification); - progressReporter!.report(nls.localize('reconnectionRunning', "Attempting to reconnect...")); - - // Register to listen for quick input is opened - disposableListener = contextKeyService.onDidChangeContext((contextKeyChangeEvent) => { - const reconnectInteraction = new Set(['inQuickOpen']); - if (contextKeyChangeEvent.affectsSome(reconnectInteraction)) { - // Need to move from dialog if being shown and user needs to type in a prompt - if (lastLocation === ProgressLocation.Dialog && progressReporter !== null) { - hideProgress(); - showProgress(ProgressLocation.Notification); - progressReporter.report(); - } - } - }); - - break; - case PersistentConnectionEventType.ReconnectionPermanentFailure: - hideProgress(); - progressReporter = null; - - dialogService.show(Severity.Error, nls.localize('reconnectionPermanentFailure', "Cannot reconnect. Please reload the window."), [nls.localize('reloadWindow', "Reload Window"), nls.localize('cancel', "Cancel")], { cancelId: 1 }).then(result => { - // Reload the window - if (result.choice === 0) { - commandService.executeCommand(ReloadWindowAction.ID); - } - }); - break; - case PersistentConnectionEventType.ConnectionGain: - hideProgress(); - progressReporter = null; - break; - } - }); - } - } -} - -class ReconnectionTimer implements IDisposable { - private readonly _progressReporter: ProgressReporter; - private readonly _completionTime: number; - private readonly _token: NodeJS.Timeout; - - constructor(progressReporter: ProgressReporter, completionTime: number) { - this._progressReporter = progressReporter; - this._completionTime = completionTime; - this._token = setInterval(() => this._render(), 1000); - this._render(); - } - - public dispose(): void { - clearInterval(this._token); - } - - private _render() { - const remainingTimeMs = this._completionTime - Date.now(); - if (remainingTimeMs < 0) { - return; - } - const remainingTime = Math.ceil(remainingTimeMs / 1000); - if (remainingTime === 1) { - this._progressReporter.report(nls.localize('reconnectionWaitOne', "Attempting to reconnect in {0} second...", remainingTime)); - } else { - this._progressReporter.report(nls.localize('reconnectionWaitMany', "Attempting to reconnect in {0} seconds...", remainingTime)); - } - } -} - class RemoteTelemetryEnablementUpdater extends Disposable implements IWorkbenchContribution { constructor( @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @@ -528,7 +341,6 @@ class RemoteEmptyWorkbenchPresentation extends Disposable implements IWorkbenchC const workbenchContributionsRegistry = Registry.as(WorkbenchContributionsExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteChannelsContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentDiagnosticListener, LifecyclePhase.Eventually); -workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteExtensionHostEnvironmentUpdater, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteWindowActiveIndicator, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteTelemetryEnablementUpdater, LifecyclePhase.Ready); -- GitLab