Skip to content

Commit

Permalink
web connection
Browse files Browse the repository at this point in the history
  • Loading branch information
alexdima committed Sep 16, 2019
1 parent 316fd80 commit 4e0de7c
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 201 deletions.
12 changes: 6 additions & 6 deletions src/vs/base/browser/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -229,10 +229,10 @@ class DomListener implements IDisposable {
}
}

export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(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<K extends keyof GlobalEventHandlersEventMap>(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);
}

Expand Down
82 changes: 77 additions & 5 deletions src/vs/platform/remote/browser/browserSocketFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,27 +26,34 @@ export interface IWebSocket {
close(): void;
}

class BrowserWebSocket implements IWebSocket {
class BrowserWebSocket extends Disposable implements IWebSocket {

private readonly _onData = new Emitter<ArrayBuffer>();
public readonly onData = this._onData.event;

public readonly onOpen: Event<void>;
public readonly onClose: Event<void>;
public readonly onError: Event<any>;

private readonly _onClose = this._register(new Emitter<void>());
public readonly onClose = this._onClose.event;

private readonly _onError = this._register(new Emitter<any>());
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;
Expand Down Expand Up @@ -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();
}
}

Expand Down
195 changes: 195 additions & 0 deletions src/vs/workbench/contrib/remote/browser/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -445,3 +454,189 @@ Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions
'View: Show Remote Explorer',
nls.localize('view', "View")
);


class ProgressReporter {
private _currentProgress: IProgress<IProgressStep> | null = null;
private lastReport: string | null = null;

constructor(currentProgress: IProgress<IProgressStep> | null) {
this._currentProgress = currentProgress;
}

set currentProgress(progress: IProgress<IProgressStep>) {
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<void>((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<string>(['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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually);
Loading

0 comments on commit 4e0de7c

Please sign in to comment.