Skip to content

Commit 4e0de7c

Browse files
committed
web connection
1 parent 316fd80 commit 4e0de7c

4 files changed

Lines changed: 280 additions & 201 deletions

File tree

src/vs/base/browser/dom.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,11 @@ export const toggleClass: (node: HTMLElement | SVGElement, className: string, sh
203203
class DomListener implements IDisposable {
204204

205205
private _handler: (e: any) => void;
206-
private _node: Element | Window | Document;
206+
private _node: EventTarget;
207207
private readonly _type: string;
208208
private readonly _options: boolean | AddEventListenerOptions;
209209

210-
constructor(node: Element | Window | Document, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
210+
constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
211211
this._node = node;
212212
this._type = type;
213213
this._handler = handler;
@@ -229,10 +229,10 @@ class DomListener implements IDisposable {
229229
}
230230
}
231231

232-
export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(node: Element | Window | Document, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable;
233-
export function addDisposableListener(node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
234-
export function addDisposableListener(node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture: AddEventListenerOptions): IDisposable;
235-
export function addDisposableListener(node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture?: boolean | AddEventListenerOptions): IDisposable {
232+
export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable;
233+
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
234+
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture: AddEventListenerOptions): IDisposable;
235+
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean | AddEventListenerOptions): IDisposable {
236236
return new DomListener(node, type, handler, useCapture);
237237
}
238238

src/vs/platform/remote/browser/browserSocketFactory.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { ISocket } from 'vs/base/parts/ipc/common/ipc.net';
88
import { VSBuffer } from 'vs/base/common/buffer';
99
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
1010
import { Event, Emitter } from 'vs/base/common/event';
11+
import * as dom from 'vs/base/browser/dom';
12+
import { RunOnceScheduler } from 'vs/base/common/async';
13+
import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
1114

1215
export interface IWebSocketFactory {
1316
create(url: string): IWebSocket;
@@ -23,27 +26,34 @@ export interface IWebSocket {
2326
close(): void;
2427
}
2528

26-
class BrowserWebSocket implements IWebSocket {
29+
class BrowserWebSocket extends Disposable implements IWebSocket {
2730

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

3134
public readonly onOpen: Event<void>;
32-
public readonly onClose: Event<void>;
33-
public readonly onError: Event<any>;
35+
36+
private readonly _onClose = this._register(new Emitter<void>());
37+
public readonly onClose = this._onClose.event;
38+
39+
private readonly _onError = this._register(new Emitter<any>());
40+
public readonly onError = this._onError.event;
3441

3542
private readonly _socket: WebSocket;
3643
private readonly _fileReader: FileReader;
3744
private readonly _queue: Blob[];
3845
private _isReading: boolean;
46+
private _isClosed: boolean;
3947

4048
private readonly _socketMessageListener: (ev: MessageEvent) => void;
4149

4250
constructor(socket: WebSocket) {
51+
super();
4352
this._socket = socket;
4453
this._fileReader = new FileReader();
4554
this._queue = [];
4655
this._isReading = false;
56+
this._isClosed = false;
4757

4858
this._fileReader.onload = (event) => {
4959
this._isReading = false;
@@ -71,17 +81,79 @@ class BrowserWebSocket implements IWebSocket {
7181
this._socket.addEventListener('message', this._socketMessageListener);
7282

7383
this.onOpen = Event.fromDOMEventEmitter(this._socket, 'open');
74-
this.onClose = Event.fromDOMEventEmitter(this._socket, 'close');
75-
this.onError = Event.fromDOMEventEmitter(this._socket, 'error');
84+
85+
// WebSockets emit error events that do not contain any real information
86+
// Our only chance of getting to the root cause of an error is to
87+
// listen to the close event which gives out some real information:
88+
// - https://www.w3.org/TR/websockets/#closeevent
89+
// - https://tools.ietf.org/html/rfc6455#section-11.7
90+
//
91+
// But the error event is emitted before the close event, so we therefore
92+
// delay the error event processing in the hope of receiving a close event
93+
// with more information
94+
95+
let pendingErrorEvent: any | null = null;
96+
97+
const sendPendingErrorNow = () => {
98+
const err = pendingErrorEvent;
99+
pendingErrorEvent = null;
100+
this._onError.fire(err);
101+
};
102+
103+
const errorRunner = this._register(new RunOnceScheduler(sendPendingErrorNow, 0));
104+
105+
const sendErrorSoon = (err: any) => {
106+
errorRunner.cancel();
107+
pendingErrorEvent = err;
108+
errorRunner.schedule();
109+
};
110+
111+
const sendErrorNow = (err: any) => {
112+
errorRunner.cancel();
113+
pendingErrorEvent = err;
114+
sendPendingErrorNow();
115+
};
116+
117+
this._register(dom.addDisposableListener(this._socket, 'close', (e: CloseEvent) => {
118+
this._isClosed = true;
119+
120+
if (pendingErrorEvent) {
121+
if (!window.navigator.onLine) {
122+
// The browser is offline => this is a temporary error which might resolve itself
123+
sendErrorNow(new RemoteAuthorityResolverError('Browser is offline', RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable, e));
124+
} else {
125+
// An error event is pending
126+
// The browser appears to be online...
127+
if (!e.wasClean) {
128+
// Let's be optimistic and hope that perhaps the server could not be reached or something
129+
sendErrorNow(new RemoteAuthorityResolverError(e.reason || `WebSocket close with status code ${e.code}`, RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable, e));
130+
} else {
131+
// this was a clean close => send existing error
132+
errorRunner.cancel();
133+
sendPendingErrorNow();
134+
}
135+
}
136+
}
137+
138+
this._onClose.fire();
139+
}));
140+
141+
this._register(dom.addDisposableListener(this._socket, 'error', sendErrorSoon));
76142
}
77143

78144
send(data: ArrayBuffer | ArrayBufferView): void {
145+
if (this._isClosed) {
146+
// Refuse to write data to closed WebSocket...
147+
return;
148+
}
79149
this._socket.send(data);
80150
}
81151

82152
close(): void {
153+
this._isClosed = true;
83154
this._socket.close();
84155
this._socket.removeEventListener('message', this._socketMessageListener);
156+
this.dispose();
85157
}
86158
}
87159

src/vs/workbench/contrib/remote/browser/remote.ts

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
3838
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
3939
import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions';
4040
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
41+
import { IProgress, IProgressStep, IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
42+
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
43+
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
44+
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
45+
import { ReconnectionWaitEvent, PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
46+
import Severity from 'vs/base/common/severity';
47+
import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions';
48+
import { IDisposable } from 'vs/base/common/lifecycle';
49+
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
4150

4251
interface HelpInformation {
4352
extensionDescription: IExtensionDescription;
@@ -445,3 +454,189 @@ Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions
445454
'View: Show Remote Explorer',
446455
nls.localize('view', "View")
447456
);
457+
458+
459+
class ProgressReporter {
460+
private _currentProgress: IProgress<IProgressStep> | null = null;
461+
private lastReport: string | null = null;
462+
463+
constructor(currentProgress: IProgress<IProgressStep> | null) {
464+
this._currentProgress = currentProgress;
465+
}
466+
467+
set currentProgress(progress: IProgress<IProgressStep>) {
468+
this._currentProgress = progress;
469+
}
470+
471+
report(message?: string) {
472+
if (message) {
473+
this.lastReport = message;
474+
}
475+
476+
if (this.lastReport && this._currentProgress) {
477+
this._currentProgress.report({ message: this.lastReport });
478+
}
479+
}
480+
}
481+
482+
class RemoteAgentConnectionStatusListener implements IWorkbenchContribution {
483+
constructor(
484+
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
485+
@IProgressService progressService: IProgressService,
486+
@IDialogService dialogService: IDialogService,
487+
@ICommandService commandService: ICommandService,
488+
@IContextKeyService contextKeyService: IContextKeyService
489+
) {
490+
const connection = remoteAgentService.getConnection();
491+
if (connection) {
492+
let currentProgressPromiseResolve: (() => void) | null = null;
493+
let progressReporter: ProgressReporter | null = null;
494+
let lastLocation: ProgressLocation | null = null;
495+
let currentTimer: ReconnectionTimer | null = null;
496+
let reconnectWaitEvent: ReconnectionWaitEvent | null = null;
497+
let disposableListener: IDisposable | null = null;
498+
499+
function showProgress(location: ProgressLocation, buttons?: string[]) {
500+
if (currentProgressPromiseResolve) {
501+
currentProgressPromiseResolve();
502+
}
503+
504+
const promise = new Promise<void>((resolve) => currentProgressPromiseResolve = resolve);
505+
lastLocation = location;
506+
507+
if (location === ProgressLocation.Dialog) {
508+
// Show dialog
509+
progressService!.withProgress(
510+
{ location: ProgressLocation.Dialog, buttons },
511+
(progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; },
512+
(choice?) => {
513+
// Handle choice from dialog
514+
if (choice === 0 && buttons && reconnectWaitEvent) {
515+
reconnectWaitEvent.skipWait();
516+
} else {
517+
showProgress(ProgressLocation.Notification, buttons);
518+
}
519+
520+
progressReporter!.report();
521+
});
522+
} else {
523+
// Show notification
524+
progressService!.withProgress(
525+
{ location: ProgressLocation.Notification, buttons },
526+
(progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; },
527+
(choice?) => {
528+
// Handle choice from notification
529+
if (choice === 0 && buttons && reconnectWaitEvent) {
530+
reconnectWaitEvent.skipWait();
531+
} else {
532+
hideProgress();
533+
}
534+
});
535+
}
536+
}
537+
538+
function hideProgress() {
539+
if (currentProgressPromiseResolve) {
540+
currentProgressPromiseResolve();
541+
}
542+
543+
currentProgressPromiseResolve = null;
544+
}
545+
546+
connection.onDidStateChange((e) => {
547+
if (currentTimer) {
548+
currentTimer.dispose();
549+
currentTimer = null;
550+
}
551+
552+
if (disposableListener) {
553+
disposableListener.dispose();
554+
disposableListener = null;
555+
}
556+
switch (e.type) {
557+
case PersistentConnectionEventType.ConnectionLost:
558+
if (!currentProgressPromiseResolve) {
559+
progressReporter = new ProgressReporter(null);
560+
showProgress(ProgressLocation.Dialog, [nls.localize('reconnectNow', "Reconnect Now")]);
561+
}
562+
563+
progressReporter!.report(nls.localize('connectionLost', "Connection Lost"));
564+
break;
565+
case PersistentConnectionEventType.ReconnectionWait:
566+
hideProgress();
567+
reconnectWaitEvent = e;
568+
showProgress(lastLocation || ProgressLocation.Notification, [nls.localize('reconnectNow', "Reconnect Now")]);
569+
currentTimer = new ReconnectionTimer(progressReporter!, Date.now() + 1000 * e.durationSeconds);
570+
break;
571+
case PersistentConnectionEventType.ReconnectionRunning:
572+
hideProgress();
573+
showProgress(lastLocation || ProgressLocation.Notification);
574+
progressReporter!.report(nls.localize('reconnectionRunning', "Attempting to reconnect..."));
575+
576+
// Register to listen for quick input is opened
577+
disposableListener = contextKeyService.onDidChangeContext((contextKeyChangeEvent) => {
578+
const reconnectInteraction = new Set<string>(['inQuickOpen']);
579+
if (contextKeyChangeEvent.affectsSome(reconnectInteraction)) {
580+
// Need to move from dialog if being shown and user needs to type in a prompt
581+
if (lastLocation === ProgressLocation.Dialog && progressReporter !== null) {
582+
hideProgress();
583+
showProgress(ProgressLocation.Notification);
584+
progressReporter.report();
585+
}
586+
}
587+
});
588+
589+
break;
590+
case PersistentConnectionEventType.ReconnectionPermanentFailure:
591+
hideProgress();
592+
progressReporter = null;
593+
594+
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 => {
595+
// Reload the window
596+
if (result.choice === 0) {
597+
commandService.executeCommand(ReloadWindowAction.ID);
598+
}
599+
});
600+
break;
601+
case PersistentConnectionEventType.ConnectionGain:
602+
hideProgress();
603+
progressReporter = null;
604+
break;
605+
}
606+
});
607+
}
608+
}
609+
}
610+
611+
class ReconnectionTimer implements IDisposable {
612+
private readonly _progressReporter: ProgressReporter;
613+
private readonly _completionTime: number;
614+
private readonly _token: any;
615+
616+
constructor(progressReporter: ProgressReporter, completionTime: number) {
617+
this._progressReporter = progressReporter;
618+
this._completionTime = completionTime;
619+
this._token = setInterval(() => this._render(), 1000);
620+
this._render();
621+
}
622+
623+
public dispose(): void {
624+
clearInterval(this._token);
625+
}
626+
627+
private _render() {
628+
const remainingTimeMs = this._completionTime - Date.now();
629+
if (remainingTimeMs < 0) {
630+
return;
631+
}
632+
const remainingTime = Math.ceil(remainingTimeMs / 1000);
633+
if (remainingTime === 1) {
634+
this._progressReporter.report(nls.localize('reconnectionWaitOne', "Attempting to reconnect in {0} second...", remainingTime));
635+
} else {
636+
this._progressReporter.report(nls.localize('reconnectionWaitMany', "Attempting to reconnect in {0} seconds...", remainingTime));
637+
}
638+
}
639+
}
640+
641+
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
642+
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually);

0 commit comments

Comments
 (0)