提交 4e0de7ce 编写于 作者: A Alex Dima

web connection

上级 316fd807
......@@ -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<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);
}
......
......@@ -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<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;
......@@ -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();
}
}
......
......@@ -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<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);
......@@ -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<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 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<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: 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<IWorkbenchContributionsRegistry>(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);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册