Merge branch 'sandy081/settingsSyncAuth'

This commit is contained in:
Sandeep Somavarapu
2019-09-27 16:16:57 +02:00
16 changed files with 599 additions and 217 deletions
@@ -58,6 +58,11 @@ import { SettingsMergeChannelClient } from 'vs/platform/userDataSync/common/sett
import { IElectronService } from 'vs/platform/electron/node/electron';
import { LoggerService } from 'vs/platform/log/node/loggerService';
import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog';
import { IAuthTokenService } from 'vs/platform/auth/common/auth';
import { AuthTokenService } from 'vs/platform/auth/common/authTokenService';
import { AuthTokenChannel } from 'vs/platform/auth/common/authTokenIpc';
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService';
export interface ISharedProcessConfiguration {
readonly machineId: string;
@@ -176,6 +181,8 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat
services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService));
services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService));
services.set(ICredentialsService, new SyncDescriptor(KeytarCredentialsService));
services.set(IAuthTokenService, new SyncDescriptor(AuthTokenService));
services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService));
const settingsMergeChannel = server.getChannel('settingsMerge', activeWindowRouter);
services.set(ISettingsMergeService, new SettingsMergeChannelClient(settingsMergeChannel));
@@ -199,6 +206,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat
const diagnosticsChannel = new DiagnosticsChannel(diagnosticsService);
server.registerChannel('diagnostics', diagnosticsChannel);
const authTokenService = accessor.get(IAuthTokenService);
const authTokenChannel = new AuthTokenChannel(authTokenService);
server.registerChannel('authToken', authTokenChannel);
const userDataSyncService = accessor.get(IUserDataSyncService);
const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService);
server.registerChannel('userDataSync', userDataSyncChannel);
+29
View File
@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
export const enum AuthTokenStatus {
Disabled = 'Disabled',
Inactive = 'Inactive',
Active = 'Active'
}
export const IAuthTokenService = createDecorator<IAuthTokenService>('IAuthTokenService');
export interface IAuthTokenService {
_serviceBrand: undefined;
readonly status: AuthTokenStatus;
readonly onDidChangeStatus: Event<AuthTokenStatus>;
getToken(): Promise<string | null>;
updateToken(token: string): Promise<void>;
refreshToken(): Promise<void>;
deleteToken(): Promise<void>;
}
@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IAuthTokenService } from 'vs/platform/auth/common/auth';
export class AuthTokenChannel implements IServerChannel {
constructor(private readonly service: IAuthTokenService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeStatus': return this.service.onDidChangeStatus;
}
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case '_getInitialStatus': return Promise.resolve(this.service.status);
case 'getToken': return this.service.getToken();
case 'updateToken': return this.service.updateToken(args[0]);
case 'refreshToken': return this.service.refreshToken();
case 'deleteToken': return this.service.deleteToken();
}
throw new Error('Invalid call');
}
}
@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { Disposable } from 'vs/base/common/lifecycle';
import { IProductService } from 'vs/platform/product/common/productService';
const SERVICE_NAME = 'VS Code';
const ACCOUNT = 'MyAccount';
export class AuthTokenService extends Disposable implements IAuthTokenService {
_serviceBrand: undefined;
private _status: AuthTokenStatus = AuthTokenStatus.Disabled;
get status(): AuthTokenStatus { return this._status; }
private _onDidChangeStatus: Emitter<AuthTokenStatus> = this._register(new Emitter<AuthTokenStatus>());
readonly onDidChangeStatus: Event<AuthTokenStatus> = this._onDidChangeStatus.event;
constructor(
@ICredentialsService private readonly credentialsService: ICredentialsService,
@IProductService productService: IProductService,
) {
super();
if (productService.settingsSyncStoreUrl) {
this._status = AuthTokenStatus.Inactive;
this.getToken().then(token => {
if (token) {
this.setStatus(AuthTokenStatus.Active);
}
});
}
}
getToken(): Promise<string | null> {
return this.credentialsService.getPassword(SERVICE_NAME, ACCOUNT);
}
async updateToken(token: string): Promise<void> {
await this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, token);
this.setStatus(AuthTokenStatus.Active);
}
async refreshToken(): Promise<void> {
await this.deleteToken();
}
async deleteToken(): Promise<void> {
await this.credentialsService.deletePassword(SERVICE_NAME, ACCOUNT);
this.setStatus(AuthTokenStatus.Inactive);
}
private setStatus(status: AuthTokenStatus): void {
if (this._status !== status) {
this._status = status;
this._onDidChangeStatus.fire(status);
}
}
}
@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const ICredentialsService = createDecorator<ICredentialsService>('ICredentialsService');
export interface ICredentialsService {
_serviceBrand: undefined;
getPassword(service: string, account: string): Promise<string | null>;
setPassword(service: string, account: string, password: string): Promise<void>;
deletePassword(service: string, account: string): Promise<boolean>;
findPassword(service: string): Promise<string | null>;
findCredentials(service: string): Promise<Array<{ account: string, password: string }>>;
}
@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { IdleValue } from 'vs/base/common/async';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
type KeytarModule = typeof import('keytar');
export class KeytarCredentialsService implements ICredentialsService {
_serviceBrand: undefined;
private readonly _keytar = new IdleValue<Promise<KeytarModule>>(() => import('keytar'));
async getPassword(service: string, account: string): Promise<string | null> {
const keytar = await this._keytar.getValue();
return keytar.getPassword(service, account);
}
async setPassword(service: string, account: string, password: string): Promise<void> {
const keytar = await this._keytar.getValue();
return keytar.setPassword(service, account, password);
}
async deletePassword(service: string, account: string): Promise<boolean> {
const keytar = await this._keytar.getValue();
return keytar.deletePassword(service, account);
}
async findPassword(service: string): Promise<string | null> {
const keytar = await this._keytar.getValue();
return keytar.findPassword(service);
}
async findCredentials(service: string): Promise<Array<{ account: string, password: string }>> {
const keytar = await this._keytar.getValue();
return keytar.findCredentials(service);
}
}
registerSingleton(ICredentialsService, KeytarCredentialsService, true);
@@ -12,6 +12,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { timeout } from 'vs/base/common/async';
import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
@@ -35,6 +36,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
) {
super();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
@@ -43,12 +45,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.updateStatus();
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal));
this._register(authTokenService.onDidChangeStatus(() => this.onDidChangeAuthTokenStatus()));
}
async sync(_continue?: boolean): Promise<boolean> {
if (!this.userDataSyncStoreService.enabled) {
throw new Error('Not enabled');
}
if (this.authTokenService.status === AuthTokenStatus.Inactive) {
return Promise.reject('Not Authenticated. Please sign in to start sync.');
}
for (const synchroniser of this.synchronisers) {
if (!await synchroniser.sync(_continue)) {
return false;
@@ -105,40 +111,58 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return null;
}
private onDidChangeAuthTokenStatus(): void {
if (this.authTokenService.status === AuthTokenStatus.Inactive) {
this.stop();
}
}
}
export class UserDataAutoSync extends Disposable {
private enabled: boolean = false;
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly userDataSyncLogService: IUserDataSyncLogService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
) {
super();
if (userDataSyncStoreService.enabled) {
this.sync(true);
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('configurationSync.enable'))(() => {
if (this.isSyncEnabled()) {
userDataSyncLogService.info('Syncing configuration started...');
this.sync(true);
} else {
this.userDataSyncService.stop();
userDataSyncLogService.info('Syncing configuration stopped.');
}
}));
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('configurationSync.enable'))(() => this.updateEnablement()));
this._register(authTokenService.onDidChangeStatus(() => this.updateEnablement()));
// Sync immediately if there is a local change.
this._register(Event.debounce(this.userDataSyncService.onDidChangeLocal, () => undefined, 500)(() => this.sync(false)));
}
}
private updateEnablement(): void {
const enabled = this.isSyncEnabled();
if (this.enabled !== enabled) {
this.enabled = enabled;
if (this.enabled) {
this.userDataSyncLogService.info('Syncing configuration started...');
this.sync(true);
} else {
this.userDataSyncService.stop();
this.userDataSyncLogService.info('Syncing configuration stopped.');
}
}
}
private async sync(loop: boolean): Promise<void> {
if (this.isSyncEnabled()) {
try {
await this.userDataSyncService.sync();
} catch (e) {
this.userDataSyncLogService.error(e);
if (this.enabled) {
if (this.authTokenService.status === AuthTokenStatus.Active) {
try {
await this.userDataSyncService.sync();
} catch (e) {
this.userDataSyncLogService.error(e);
}
}
if (loop) {
await timeout(1000 * 5); // Loop sync for every 5s.
@@ -148,7 +172,7 @@ export class UserDataAutoSync extends Disposable {
}
private isSyncEnabled(): boolean {
return this.configurationService.getValue<boolean>('configurationSync.enable');
return this.configurationService.getValue<boolean>('configurationSync.enable') && this.authTokenService.status !== AuthTokenStatus.Inactive;
}
}
@@ -12,6 +12,7 @@ import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
import { IAuthTokenService } from 'vs/platform/auth/common/auth';
export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService {
@@ -28,6 +29,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
@IProductService private readonly productService: IProductService,
@IRequestService private readonly requestService: IRequestService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
) {
super();
}
@@ -98,11 +100,19 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
const authToken = await this.authTokenService.getToken();
if (!authToken) {
return Promise.reject(new Error('No Auth Token Available.'));
}
options.headers = options.headers || {};
options.headers['authorization'] = `Bearer ${authToken}`;
const context = await this.requestService.request(options, token);
if (context.res.statusCode === 401) {
// Not Authorized
this.logService.info('Authroization Failed.');
this.authTokenService.refreshToken();
Promise.reject('Authroization Failed.');
}
@@ -5,7 +5,7 @@
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { MainContext, MainThreadKeytarShape, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
@extHostNamedCustomer(MainContext.MainThreadKeytar)
export class MainThreadKeytar implements MainThreadKeytarShape {
@@ -4,32 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IUserDataSyncService, SyncStatus, SyncSource, CONTEXT_SYNC_STATE, registerConfiguration } from 'vs/platform/userDataSync/common/userDataSync';
import { localize } from 'vs/nls';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { registerConfiguration } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { MenuRegistry, MenuId, IMenuItem } from 'vs/platform/actions/common/actions';
import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity';
import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { URI } from 'vs/base/common/uri';
import { registerAndGetAmdImageURL } from 'vs/base/common/amd';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { Event } from 'vs/base/common/event';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { isEqual } from 'vs/base/common/resources';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { isWeb } from 'vs/base/common/platform';
import { UserDataAutoSync } from 'vs/platform/userDataSync/common/userDataSyncService';
import { IProductService } from 'vs/platform/product/common/productService';
import { IEditorInput } from 'vs/workbench/common/editor';
import { UserDataSyncWorkbenchContribution } from 'vs/workbench/contrib/userDataSync/browser/userDataSync';
class UserDataSyncConfigurationContribution implements IWorkbenchContribution {
@@ -54,185 +37,8 @@ class UserDataAutoSyncContribution extends Disposable implements IWorkbenchContr
}
}
const SYNC_PUSH_LIGHT_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userDataSync/browser/media/check-light.svg`));
const SYNC_PUSH_DARK_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userDataSync/browser/media/check-dark.svg`));
class SyncActionsContribution extends Disposable implements IWorkbenchContribution {
private readonly syncEnablementContext: IContextKey<string>;
private readonly badgeDisposable = this._register(new MutableDisposable());
private readonly conflictsWarningDisposable = this._register(new MutableDisposable());
constructor(
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IContextKeyService contextKeyService: IContextKeyService,
@IActivityService private readonly activityService: IActivityService,
@INotificationService private readonly notificationService: INotificationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@ITextFileService private readonly textFileService: ITextFileService,
@IHistoryService private readonly historyService: IHistoryService,
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
) {
super();
this.syncEnablementContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
this.onDidChangeStatus(userDataSyncService.status);
this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(status => this.onDidChangeStatus(userDataSyncService.status)));
this.registerActions();
}
private onDidChangeStatus(status: SyncStatus) {
this.syncEnablementContext.set(status);
let badge: IBadge | undefined = undefined;
let clazz: string | undefined;
if (status === SyncStatus.HasConflicts) {
badge = new NumberBadge(1, () => localize('resolve conflicts', "Resolve Conflicts"));
} else if (status === SyncStatus.Syncing) {
badge = new ProgressBadge(() => localize('syncing', "Synchronising User Configuration..."));
clazz = 'progress-badge';
}
this.badgeDisposable.clear();
if (badge) {
this.badgeDisposable.value = this.activityService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz);
}
if (status === SyncStatus.HasConflicts) {
if (!this.conflictsWarningDisposable.value) {
const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts. Please resolve them to continue."),
[
{
label: localize('resolve', "Resolve Conflicts"),
run: () => this.handleConflicts()
}
]);
this.conflictsWarningDisposable.value = toDisposable(() => handle.close());
handle.onDidClose(() => this.conflictsWarningDisposable.clear());
}
} else {
const previewEditorInput = this.getPreviewEditorInput();
if (previewEditorInput) {
previewEditorInput.dispose();
}
this.conflictsWarningDisposable.clear();
}
}
private async continueSync(): Promise<void> {
// Get the preview editor
const previewEditorInput = this.getPreviewEditorInput();
// Save the preview
if (previewEditorInput && previewEditorInput.isDirty()) {
await this.textFileService.save(previewEditorInput.getResource()!);
}
try {
// Continue Sync
await this.userDataSyncService.sync(true);
} catch (error) {
this.notificationService.error(error);
return;
}
// Close the preview editor
if (previewEditorInput) {
previewEditorInput.dispose();
}
}
private getPreviewEditorInput(): IEditorInput | undefined {
return this.editorService.editors.filter(input => isEqual(input.getResource(), this.workbenchEnvironmentService.settingsSyncPreviewResource))[0];
}
private async handleConflicts(): Promise<void> {
if (this.userDataSyncService.conflictsSource === SyncSource.Settings) {
const resourceInput = {
resource: this.workbenchEnvironmentService.settingsSyncPreviewResource,
options: {
preserveFocus: false,
pinned: false,
revealIfVisible: true,
},
mode: 'jsonc'
};
this.editorService.openEditor(resourceInput)
.then(editor => {
this.historyService.remove(resourceInput);
if (editor && editor.input) {
// Trigger sync after closing the conflicts editor.
const disposable = editor.input.onDispose(() => {
disposable.dispose();
this.userDataSyncService.sync(true);
});
}
});
}
}
private registerActions(): void {
const startSyncMenuItem: IMenuItem = {
group: '5_sync',
command: {
id: 'workbench.userData.actions.syncStart',
title: localize('start sync', "Configuration Sync: Turn On")
},
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.not('config.configurationSync.enable')),
};
CommandsRegistry.registerCommand(startSyncMenuItem.command.id, () => this.configurationService.updateValue('configurationSync.enable', true));
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, startSyncMenuItem);
MenuRegistry.appendMenuItem(MenuId.CommandPalette, startSyncMenuItem);
const stopSyncMenuItem: IMenuItem = {
group: '5_sync',
command: {
id: 'workbench.userData.actions.stopSync',
title: localize('stop sync', "Configuration Sync: Turn Off")
},
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has('config.configurationSync.enable')),
};
CommandsRegistry.registerCommand(stopSyncMenuItem.command.id, () => this.configurationService.updateValue('configurationSync.enable', false));
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, stopSyncMenuItem);
MenuRegistry.appendMenuItem(MenuId.CommandPalette, stopSyncMenuItem);
const resolveConflictsMenuItem: IMenuItem = {
group: '5_sync',
command: {
id: 'sync.resolveConflicts',
title: localize('resolveConflicts', "Configuration Sync: Resolve Conflicts"),
},
when: CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts),
};
CommandsRegistry.registerCommand(resolveConflictsMenuItem.command.id, () => this.handleConflicts());
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, resolveConflictsMenuItem);
MenuRegistry.appendMenuItem(MenuId.CommandPalette, resolveConflictsMenuItem);
const continueSyncCommandId = 'workbench.userData.actions.continueSync';
CommandsRegistry.registerCommand(continueSyncCommandId, () => this.continueSync());
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: continueSyncCommandId,
title: localize('continue sync', "Configuration Sync: Continue")
},
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts)),
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: continueSyncCommandId,
title: localize('continue sync', "Configuration Sync: Continue"),
iconLocation: {
light: SYNC_PUSH_LIGHT_ICON_URI,
dark: SYNC_PUSH_DARK_ICON_URI
}
},
group: 'navigation',
order: 1,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), ResourceContextKey.Resource.isEqualTo(this.workbenchEnvironmentService.settingsSyncPreviewResource.toString())),
});
}
}
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(UserDataSyncConfigurationContribution, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(SyncActionsContribution, LifecyclePhase.Restored);
workbenchRegistry.registerWorkbenchContribution(UserDataSyncWorkbenchContribution, LifecyclePhase.Restored);
workbenchRegistry.registerWorkbenchContribution(UserDataAutoSyncContribution, LifecyclePhase.Restored);
@@ -0,0 +1,283 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IUserDataSyncService, SyncStatus, SyncSource, CONTEXT_SYNC_STATE } from 'vs/platform/userDataSync/common/userDataSync';
import { localize } from 'vs/nls';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { MenuRegistry, MenuId, IMenuItem } from 'vs/platform/actions/common/actions';
import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity';
import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { URI } from 'vs/base/common/uri';
import { registerAndGetAmdImageURL } from 'vs/base/common/amd';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { Event } from 'vs/base/common/event';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { isEqual } from 'vs/base/common/resources';
import { IEditorInput } from 'vs/workbench/common/editor';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { timeout } from 'vs/base/common/async';
const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey<string>('authTokenStatus', AuthTokenStatus.Inactive);
const SYNC_PUSH_LIGHT_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userDataSync/browser/media/check-light.svg`));
const SYNC_PUSH_DARK_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userDataSync/browser/media/check-dark.svg`));
export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution {
private readonly syncStatusContext: IContextKey<string>;
private readonly authTokenContext: IContextKey<string>;
private readonly badgeDisposable = this._register(new MutableDisposable());
private readonly conflictsWarningDisposable = this._register(new MutableDisposable());
private readonly signInNotificationDisposable = this._register(new MutableDisposable());
constructor(
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
@IContextKeyService contextKeyService: IContextKeyService,
@IActivityService private readonly activityService: IActivityService,
@INotificationService private readonly notificationService: INotificationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@ITextFileService private readonly textFileService: ITextFileService,
@IHistoryService private readonly historyService: IHistoryService,
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
) {
super();
this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
this.authTokenContext = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService);
this.onDidChangeAuthTokenStatus(this.authTokenService.status);
this.onDidChangeSyncStatus(this.userDataSyncService.status);
this._register(Event.debounce(authTokenService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeAuthTokenStatus(this.authTokenService.status)));
this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status)));
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('configurationSync.enable'))(() => this.updateBadge()));
this.registerActions();
timeout(2000).then(() => {
if (this.authTokenService.status === AuthTokenStatus.Inactive && configurationService.getValue<boolean>('configurationSync.enable')) {
this.showSignInNotification();
}
});
}
private onDidChangeAuthTokenStatus(status: AuthTokenStatus) {
this.authTokenContext.set(status);
if (status === AuthTokenStatus.Active) {
this.signInNotificationDisposable.clear();
}
this.updateBadge();
}
private onDidChangeSyncStatus(status: SyncStatus) {
this.syncStatusContext.set(status);
this.updateBadge();
if (this.userDataSyncService.status === SyncStatus.HasConflicts) {
if (!this.conflictsWarningDisposable.value) {
const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts. Please resolve them to continue."),
[
{
label: localize('resolve', "Resolve Conflicts"),
run: () => this.handleConflicts()
}
]);
this.conflictsWarningDisposable.value = toDisposable(() => handle.close());
handle.onDidClose(() => this.conflictsWarningDisposable.clear());
}
} else {
const previewEditorInput = this.getPreviewEditorInput();
if (previewEditorInput) {
previewEditorInput.dispose();
}
this.conflictsWarningDisposable.clear();
}
}
private updateBadge(): void {
this.badgeDisposable.clear();
let badge: IBadge | undefined = undefined;
let clazz: string | undefined;
if (this.authTokenService.status === AuthTokenStatus.Inactive && this.configurationService.getValue<boolean>('configurationSync.enable')) {
badge = new NumberBadge(1, () => localize('sign in', "Sign in..."));
} else if (this.userDataSyncService.status === SyncStatus.HasConflicts) {
badge = new NumberBadge(1, () => localize('resolve conflicts', "Resolve Conflicts"));
} else if (this.userDataSyncService.status === SyncStatus.Syncing) {
badge = new ProgressBadge(() => localize('syncing', "Synchronising User Configuration..."));
clazz = 'progress-badge';
}
if (badge) {
this.badgeDisposable.value = this.activityService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz);
}
}
private showSignInNotification(): void {
const handle = this.notificationService.prompt(Severity.Info, localize('show sign in', "Please sign in to Settings Sync service to start syncing configuration."),
[
{
label: localize('sign in', "Sign in..."),
run: () => this.signIn()
}
]);
this.signInNotificationDisposable.value = toDisposable(() => handle.close());
handle.onDidClose(() => this.signInNotificationDisposable.clear());
}
private async signIn(): Promise<void> {
const token = await this.quickInputService.input({ placeHolder: localize('enter token', "Please provide the auth bearer token"), ignoreFocusLost: true, });
if (token) {
await this.authTokenService.updateToken(token);
}
}
private async signOut(): Promise<void> {
await this.authTokenService.deleteToken();
}
private async continueSync(): Promise<void> {
// Get the preview editor
const previewEditorInput = this.getPreviewEditorInput();
// Save the preview
if (previewEditorInput && previewEditorInput.isDirty()) {
await this.textFileService.save(previewEditorInput.getResource()!);
}
try {
// Continue Sync
await this.userDataSyncService.sync(true);
} catch (error) {
this.notificationService.error(error);
return;
}
// Close the preview editor
if (previewEditorInput) {
previewEditorInput.dispose();
}
}
private getPreviewEditorInput(): IEditorInput | undefined {
return this.editorService.editors.filter(input => isEqual(input.getResource(), this.workbenchEnvironmentService.settingsSyncPreviewResource))[0];
}
private async handleConflicts(): Promise<void> {
if (this.userDataSyncService.conflictsSource === SyncSource.Settings) {
const resourceInput = {
resource: this.workbenchEnvironmentService.settingsSyncPreviewResource,
options: {
preserveFocus: false,
pinned: false,
revealIfVisible: true,
},
mode: 'jsonc'
};
this.editorService.openEditor(resourceInput)
.then(editor => {
this.historyService.remove(resourceInput);
if (editor && editor.input) {
// Trigger sync after closing the conflicts editor.
const disposable = editor.input.onDispose(() => {
disposable.dispose();
this.userDataSyncService.sync(true);
});
}
});
}
}
private registerActions(): void {
const signInMenuItem: IMenuItem = {
group: '5_sync',
command: {
id: 'workbench.userData.actions.login',
title: localize('sign in', "Sign in...")
},
when: ContextKeyExpr.and(CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.Inactive), ContextKeyExpr.has('config.configurationSync.enable')),
};
CommandsRegistry.registerCommand(signInMenuItem.command.id, () => this.signIn());
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, signInMenuItem);
MenuRegistry.appendMenuItem(MenuId.CommandPalette, signInMenuItem);
const signOutMenuItem: IMenuItem = {
command: {
id: 'workbench.userData.actions.logout',
title: localize('sign out', "Sign Out")
},
when: ContextKeyExpr.and(CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.Active)),
};
CommandsRegistry.registerCommand(signOutMenuItem.command.id, () => this.signOut());
MenuRegistry.appendMenuItem(MenuId.CommandPalette, signOutMenuItem);
const startSyncMenuItem: IMenuItem = {
group: '5_sync',
command: {
id: 'workbench.userData.actions.syncStart',
title: localize('start sync', "Configuration Sync: Turn On")
},
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.not('config.configurationSync.enable')),
};
CommandsRegistry.registerCommand(startSyncMenuItem.command.id, () => this.configurationService.updateValue('configurationSync.enable', true));
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, startSyncMenuItem);
MenuRegistry.appendMenuItem(MenuId.CommandPalette, startSyncMenuItem);
const stopSyncMenuItem: IMenuItem = {
group: '5_sync',
command: {
id: 'workbench.userData.actions.stopSync',
title: localize('stop sync', "Configuration Sync: Turn Off")
},
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has('config.configurationSync.enable')),
};
CommandsRegistry.registerCommand(stopSyncMenuItem.command.id, () => this.configurationService.updateValue('configurationSync.enable', false));
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, stopSyncMenuItem);
MenuRegistry.appendMenuItem(MenuId.CommandPalette, stopSyncMenuItem);
const resolveConflictsMenuItem: IMenuItem = {
group: '5_sync',
command: {
id: 'sync.resolveConflicts',
title: localize('resolveConflicts', "Configuration Sync: Resolve Conflicts"),
},
when: CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts),
};
CommandsRegistry.registerCommand(resolveConflictsMenuItem.command.id, () => this.handleConflicts());
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, resolveConflictsMenuItem);
MenuRegistry.appendMenuItem(MenuId.CommandPalette, resolveConflictsMenuItem);
const continueSyncCommandId = 'workbench.userData.actions.continueSync';
CommandsRegistry.registerCommand(continueSyncCommandId, () => this.continueSync());
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: continueSyncCommandId,
title: localize('continue sync', "Configuration Sync: Continue")
},
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts)),
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: continueSyncCommandId,
title: localize('continue sync', "Configuration Sync: Continue"),
iconLocation: {
light: SYNC_PUSH_LIGHT_ICON_URI,
dark: SYNC_PUSH_DARK_ICON_URI
}
},
group: 'navigation',
order: 1,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), ResourceContextKey.Resource.isEqualTo(this.workbenchEnvironmentService.settingsSyncPreviewResource.toString())),
});
}
}
@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
export class AuthTokenService extends Disposable implements IAuthTokenService {
_serviceBrand: undefined;
private readonly channel: IChannel;
private _status: AuthTokenStatus = AuthTokenStatus.Disabled;
get status(): AuthTokenStatus { return this._status; }
private _onDidChangeStatus: Emitter<AuthTokenStatus> = this._register(new Emitter<AuthTokenStatus>());
readonly onDidChangeStatus: Event<AuthTokenStatus> = this._onDidChangeStatus.event;
constructor(
@ISharedProcessService sharedProcessService: ISharedProcessService
) {
super();
this.channel = sharedProcessService.getChannel('authToken');
this.channel.call<AuthTokenStatus>('_getInitialStatus').then(status => {
this.updateStatus(status);
this._register(this.channel.listen<AuthTokenStatus>('onDidChangeStatus')(status => this.updateStatus(status)));
});
}
getToken(): Promise<string> {
return this.channel.call('getToken');
}
updateToken(token: string): Promise<void> {
return this.channel.call('updateToken', [token]);
}
refreshToken(): Promise<void> {
return this.channel.call('getToken');
}
deleteToken(): Promise<void> {
return this.channel.call('deleteToken');
}
private async updateStatus(status: AuthTokenStatus): Promise<void> {
this._status = status;
this._onDidChangeStatus.fire(status);
}
}
registerSingleton(IAuthTokenService, AuthTokenService);
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { IdleValue } from 'vs/base/common/async';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
+2 -1
View File
@@ -46,11 +46,12 @@ import 'vs/workbench/services/extensionManagement/node/extensionManagementServic
import 'vs/workbench/services/accessibility/node/accessibilityService';
import 'vs/workbench/services/remote/node/tunnelService';
import 'vs/workbench/services/backup/node/backupFileService';
import 'vs/workbench/services/credentials/node/credentialsService';
import 'vs/platform/credentials/node/credentialsService';
import 'vs/workbench/services/url/electron-browser/urlService';
import 'vs/workbench/services/workspaces/electron-browser/workspacesService';
import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService';
import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService';
import 'vs/workbench/services/authToken/electron-browser/authTokenService';
import 'vs/workbench/services/host/electron-browser/desktopHostService';
import 'vs/workbench/services/request/electron-browser/requestService';
import 'vs/workbench/services/lifecycle/electron-browser/lifecycleService';
+3
View File
@@ -62,6 +62,8 @@ import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { NoOpTunnelService } from 'vs/platform/remote/common/tunnelService';
import { ILoggerService } from 'vs/platform/log/common/log';
import { FileLoggerService } from 'vs/platform/log/common/fileLogService';
import { IAuthTokenService } from 'vs/platform/auth/common/auth';
import { AuthTokenService } from 'vs/platform/auth/common/authTokenService';
import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog';
import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
@@ -73,6 +75,7 @@ registerSingleton(IAccessibilityService, BrowserAccessibilityService, true);
registerSingleton(IContextMenuService, ContextMenuService);
registerSingleton(ITunnelService, NoOpTunnelService, true);
registerSingleton(ILoggerService, FileLoggerService);
registerSingleton(IAuthTokenService, AuthTokenService);
registerSingleton(IUserDataSyncLogService, UserDataSyncLogService);
registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService);
registerSingleton(IUserDataSyncService, UserDataSyncService);