Add remote.contribution

This commit is contained in:
Alex Dima 2019-05-22 11:50:39 +02:00
parent a7c7bdd32a
commit 9bcd00284b
3 changed files with 435 additions and 0 deletions

View file

@ -110,6 +110,10 @@
"name": "vs/workbench/contrib/quickopen",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/remote",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/relauncher",
"project": "vscode-workbench"

View file

@ -0,0 +1,428 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { RemoteExtensionLogFileName, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { MenuId, IMenuService, MenuItemAction, IMenu } from 'vs/platform/actions/common/actions';
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions';
import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/contrib/output/common/output';
import * as resources from 'vs/base/common/resources';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar';
import { ILabelService } from 'vs/platform/label/common/label';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { ILogService } from 'vs/platform/log/common/log';
import { IFileService } from 'vs/platform/files/common/files';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { DialogChannel } from 'vs/platform/dialogs/node/dialogIpc';
import { DownloadServiceChannel } from 'vs/platform/download/node/downloadIpc';
import { LogLevelSetterChannel } from 'vs/platform/log/node/logIpc';
import { ipcRenderer as ipc } from 'electron';
import { IDiagnosticInfoOptions, IRemoteDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IProgressService2, IProgress, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress';
import { PersistenConnectionEventType } 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 { ReloadWindowAction } from 'vs/workbench/electron-browser/actions/windowActions';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
const WINDOW_ACTIONS_COMMAND_ID = 'remote.showActions';
export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenchContribution {
private windowIndicatorEntry: IStatusbarEntryAccessor | undefined;
private windowCommandMenu: IMenu;
private hasWindowActions: boolean = false;
private remoteAuthority: string | undefined;
private disconnected: boolean = false;
constructor(
@IStatusbarService private readonly statusbarService: IStatusbarService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@ILabelService private readonly labelService: ILabelService,
@IContextKeyService private contextKeyService: IContextKeyService,
@IMenuService private menuService: IMenuService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@ICommandService private readonly commandService: ICommandService,
@IExtensionService extensionService: IExtensionService,
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService
) {
super();
this.windowCommandMenu = this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService);
this._register(this.windowCommandMenu);
this._register(CommandsRegistry.registerCommand(WINDOW_ACTIONS_COMMAND_ID, _ => this.showIndicatorActions(this.windowCommandMenu)));
this.remoteAuthority = environmentService.configuration.remoteAuthority;
if (this.remoteAuthority) {
// Pending entry until extensions are ready
this.renderWindowIndicator(nls.localize('host.open', "$(sync~spin) Opening Remote..."));
}
extensionService.whenInstalledExtensionsRegistered().then(_ => {
if (this.remoteAuthority) {
this._register(this.labelService.onDidChangeFormatters(e => this.updateWindowIndicator()));
remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority).then(() => this.setDisconnected(false), () => this.setDisconnected(true));
}
this._register(this.windowCommandMenu.onDidChange(e => this.updateWindowActions()));
this.updateWindowIndicator();
});
const connection = remoteAgentService.getConnection();
if (connection) {
this._register(connection.onDidStateChange((e) => {
switch (e.type) {
case PersistenConnectionEventType.ConnectionLost:
case PersistenConnectionEventType.ReconnectionPermanentFailure:
case PersistenConnectionEventType.ReconnectionRunning:
case PersistenConnectionEventType.ReconnectionWait:
this.setDisconnected(true);
break;
case PersistenConnectionEventType.ConnectionGain:
this.setDisconnected(false);
break;
}
}));
}
}
private setDisconnected(isDisconnected: boolean): void {
if (this.disconnected !== isDisconnected) {
this.disconnected = isDisconnected;
this.updateWindowIndicator();
}
}
private updateWindowIndicator(): void {
const windowActionCommand = this.windowCommandMenu.getActions().length ? WINDOW_ACTIONS_COMMAND_ID : undefined;
if (this.remoteAuthority) {
const hostLabel = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.remoteAuthority) || this.remoteAuthority;
if (!this.disconnected) {
this.renderWindowIndicator(`$(remote) ${hostLabel}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel), windowActionCommand);
} else {
this.renderWindowIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from")} ${hostLabel}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel));
}
} else {
if (windowActionCommand) {
this.renderWindowIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a remote window"), windowActionCommand);
} else if (this.windowIndicatorEntry) {
this.windowIndicatorEntry.dispose();
this.windowIndicatorEntry = undefined;
}
}
}
private updateWindowActions() {
const newHasWindowActions = this.windowCommandMenu.getActions().length > 0;
if (newHasWindowActions !== this.hasWindowActions) {
this.hasWindowActions = newHasWindowActions;
this.updateWindowIndicator();
}
}
private renderWindowIndicator(text: string, tooltip?: string, command?: string): void {
const properties: IStatusbarEntry = {
backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), text, tooltip, command
};
if (this.windowIndicatorEntry) {
this.windowIndicatorEntry.update(properties);
} else {
this.windowIndicatorEntry = this.statusbarService.addEntry(properties, StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */);
}
}
private showIndicatorActions(menu: IMenu) {
const actions = menu.getActions();
const items: (IQuickPickItem | IQuickPickSeparator)[] = [];
for (let actionGroup of actions) {
if (items.length) {
items.push({ type: 'separator' });
}
for (let action of actionGroup[1]) {
if (action instanceof MenuItemAction) {
let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value;
if (action.item.category) {
const category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value;
label = nls.localize('cat.title', "{0}: {1}", category, label);
}
items.push({
type: 'item',
id: action.item.id,
label
});
}
}
}
const quickPick = this.quickInputService.createQuickPick();
quickPick.items = items;
quickPick.canSelectMany = false;
quickPick.onDidAccept(_ => {
const selectedItems = quickPick.selectedItems;
if (selectedItems.length === 1) {
this.commandService.executeCommand(selectedItems[0].id!);
}
quickPick.hide();
});
quickPick.show();
}
}
class LogOutputChannels extends Disposable implements IWorkbenchContribution {
constructor(
@IRemoteAgentService remoteAgentService: IRemoteAgentService
) {
super();
remoteAgentService.getEnvironment().then(remoteEnv => {
if (remoteEnv) {
const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(OutputExt.OutputChannels);
outputChannelRegistry.registerChannel({ id: 'remoteExtensionLog', label: nls.localize('remoteExtensionLog', "Remote Server"), file: resources.joinPath(remoteEnv.logsPath, `${RemoteExtensionLogFileName}.log`), log: true });
}
});
}
}
class RemoteChannelsContribution implements IWorkbenchContribution {
constructor(
@ILogService logService: ILogService,
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
@IFileService fileService: IFileService,
@IDialogService dialogService: IDialogService
) {
const connection = remoteAgentService.getConnection();
if (connection) {
connection.registerChannel('dialog', new DialogChannel(dialogService));
connection.registerChannel('download', new DownloadServiceChannel());
connection.registerChannel('loglevel', new LogLevelSetterChannel(logService));
}
}
}
class RemoteAgentDiagnosticListener implements IWorkbenchContribution {
constructor(
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
@ILabelService labelService: ILabelService
) {
ipc.on('vscode:getDiagnosticInfo', (event: Event, request: { replyChannel: string, args: IDiagnosticInfoOptions }): void => {
const connection = remoteAgentService.getConnection();
if (connection) {
const hostName = labelService.getHostLabel(REMOTE_HOST_SCHEME, connection.remoteAuthority);
remoteAgentService.getDiagnosticInfo(request.args)
.then(info => {
if (info) {
(info as IRemoteDiagnosticInfo).hostName = hostName;
}
ipc.send(request.replyChannel, info);
})
.catch(e => {
const errorMessage = e && e.message ? `Fetching remote diagnostics for '${hostName}' failed: ${e.message}` : `Fetching remote diagnostics for '${hostName}' failed.`;
ipc.send(request.replyChannel, { hostName, errorMessage });
});
} else {
ipc.send(request.replyChannel);
}
});
}
}
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,
@IProgressService2 progressService: IProgressService2,
@IDialogService dialogService: IDialogService,
@ICommandService commandService: ICommandService
) {
const connection = remoteAgentService.getConnection();
if (connection) {
let currentProgressPromiseResolve: (() => void) | null = null;
let progressReporter: ProgressReporter | null = null;
let currentTimer: ReconnectionTimer | null = null;
connection.onDidStateChange((e) => {
if (currentTimer) {
currentTimer.dispose();
currentTimer = null;
}
switch (e.type) {
case PersistenConnectionEventType.ConnectionLost:
if (!currentProgressPromiseResolve) {
let promise = new Promise<void>((resolve) => currentProgressPromiseResolve = resolve);
progressService!.withProgress(
{ location: ProgressLocation.Dialog },
(progress: IProgress<IProgressStep> | null) => { progressReporter = new ProgressReporter(progress!); return promise; },
() => {
currentProgressPromiseResolve!();
promise = new Promise<void>((resolve) => currentProgressPromiseResolve = resolve);
progressService!.withProgress({ location: ProgressLocation.Notification }, (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; });
progressReporter!.report();
}
);
}
progressReporter!.report(nls.localize('connectionLost', "Connection Lost"));
break;
case PersistenConnectionEventType.ReconnectionWait:
currentTimer = new ReconnectionTimer(progressReporter!, Date.now() + 1000 * e.durationSeconds);
break;
case PersistenConnectionEventType.ReconnectionRunning:
progressReporter!.report(nls.localize('reconnectionRunning', "Attempting to reconnect..."));
break;
case PersistenConnectionEventType.ReconnectionPermanentFailure:
currentProgressPromiseResolve!();
currentProgressPromiseResolve = null;
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(choice => {
// Reload the window
if (choice === 0) {
commandService.executeCommand(ReloadWindowAction.ID);
}
});
break;
case PersistenConnectionEventType.ConnectionGain:
currentProgressPromiseResolve!();
currentProgressPromiseResolve = null;
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,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super();
this.updateRemoteTelemetryEnablement();
this._register(configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('telemetry.enableTelemetry')) {
this.updateRemoteTelemetryEnablement();
}
}));
}
private updateRemoteTelemetryEnablement(): Promise<void> {
if (!this.configurationService.getValue('telemetry.enableTelemetry')) {
return this.remoteAgentService.disableTelemetry();
}
return Promise.resolve();
}
}
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchContributionsExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteChannelsContribution, LifecyclePhase.Starting);
workbenchContributionsRegistry.registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Eventually);
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentDiagnosticListener, LifecyclePhase.Eventually);
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually);
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteWindowActiveIndicator, LifecyclePhase.Starting);
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteTelemetryEnablementUpdater, LifecyclePhase.Ready);
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
.registerConfiguration({
id: 'remote',
title: nls.localize('remote', "Remote"),
type: 'object',
properties: {
'remote.extensionKind': {
type: 'object',
markdownDescription: nls.localize('remote.extensionKind', "Override the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote. By overriding an extension's default kind using this setting, you specify if that extension should be installed and enabled locally or remotely."),
patternProperties: {
'([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$': {
type: 'string',
enum: [
'ui',
'workspace'
],
enumDescriptions: [
nls.localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."),
nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.")
],
default: 'ui'
},
},
default: {
'pub.name': 'ui'
}
}
}
});

View file

@ -264,6 +264,9 @@ import 'vs/workbench/contrib/relauncher/electron-browser/relauncher.contribution
// Tasks
import 'vs/workbench/contrib/tasks/electron-browser/task.contribution';
// Remote
import 'vs/workbench/contrib/remote/electron-browser/remote.contribution';
// Emmet
import 'vs/workbench/contrib/emmet/browser/emmet.contribution';