diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 92215650a8b..9ddf3c0eca9 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -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); diff --git a/src/vs/platform/auth/common/auth.ts b/src/vs/platform/auth/common/auth.ts new file mode 100644 index 00000000000..7b2961d30b0 --- /dev/null +++ b/src/vs/platform/auth/common/auth.ts @@ -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'); + +export interface IAuthTokenService { + _serviceBrand: undefined; + + readonly status: AuthTokenStatus; + readonly onDidChangeStatus: Event; + + getToken(): Promise; + updateToken(token: string): Promise; + refreshToken(): Promise; + deleteToken(): Promise; + +} + diff --git a/src/vs/platform/auth/common/authTokenIpc.ts b/src/vs/platform/auth/common/authTokenIpc.ts new file mode 100644 index 00000000000..a5c78c4a8dd --- /dev/null +++ b/src/vs/platform/auth/common/authTokenIpc.ts @@ -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 { + switch (event) { + case 'onDidChangeStatus': return this.service.onDidChangeStatus; + } + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + 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'); + } +} diff --git a/src/vs/platform/auth/common/authTokenService.ts b/src/vs/platform/auth/common/authTokenService.ts new file mode 100644 index 00000000000..3d5f5a5437b --- /dev/null +++ b/src/vs/platform/auth/common/authTokenService.ts @@ -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 = this._register(new Emitter()); + readonly onDidChangeStatus: Event = 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 { + return this.credentialsService.getPassword(SERVICE_NAME, ACCOUNT); + } + + async updateToken(token: string): Promise { + await this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, token); + this.setStatus(AuthTokenStatus.Active); + } + + async refreshToken(): Promise { + await this.deleteToken(); + } + + async deleteToken(): Promise { + 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); + } + } + +} + diff --git a/src/vs/platform/credentials/common/credentials.ts b/src/vs/platform/credentials/common/credentials.ts new file mode 100644 index 00000000000..6fcb7dffaa0 --- /dev/null +++ b/src/vs/platform/credentials/common/credentials.ts @@ -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'); + +export interface ICredentialsService { + + _serviceBrand: undefined; + + getPassword(service: string, account: string): Promise; + setPassword(service: string, account: string, password: string): Promise; + deletePassword(service: string, account: string): Promise; + findPassword(service: string): Promise; + findCredentials(service: string): Promise>; +} diff --git a/src/vs/platform/credentials/node/credentialsService.ts b/src/vs/platform/credentials/node/credentialsService.ts new file mode 100644 index 00000000000..8c876d5b759 --- /dev/null +++ b/src/vs/platform/credentials/node/credentialsService.ts @@ -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>(() => import('keytar')); + + async getPassword(service: string, account: string): Promise { + const keytar = await this._keytar.getValue(); + return keytar.getPassword(service, account); + } + + async setPassword(service: string, account: string, password: string): Promise { + const keytar = await this._keytar.getValue(); + return keytar.setPassword(service, account, password); + } + + async deletePassword(service: string, account: string): Promise { + const keytar = await this._keytar.getValue(); + return keytar.deletePassword(service, account); + } + + async findPassword(service: string): Promise { + const keytar = await this._keytar.getValue(); + return keytar.findPassword(service); + } + + async findCredentials(service: string): Promise> { + const keytar = await this._keytar.getValue(); + return keytar.findCredentials(service); + } +} + +registerSingleton(ICredentialsService, KeytarCredentialsService, true); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 3499179682a..7fe3a290957 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -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 { 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 { - 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('configurationSync.enable'); + return this.configurationService.getValue('configurationSync.enable') && this.authTokenService.status !== AuthTokenStatus.Inactive; } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index aac3620d9e4..f5c0a1432ca 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -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 { + 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.'); } diff --git a/src/vs/workbench/api/browser/mainThreadKeytar.ts b/src/vs/workbench/api/browser/mainThreadKeytar.ts index ddcf676a10c..5312a9aad19 100644 --- a/src/vs/workbench/api/browser/mainThreadKeytar.ts +++ b/src/vs/workbench/api/browser/mainThreadKeytar.ts @@ -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 { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index c3bbbd867f4..3fe61e9a7e5 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -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; - 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 { - // 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 { - 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(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(UserDataSyncConfigurationContribution, LifecyclePhase.Starting); -workbenchRegistry.registerWorkbenchContribution(SyncActionsContribution, LifecyclePhase.Restored); +workbenchRegistry.registerWorkbenchContribution(UserDataSyncWorkbenchContribution, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(UserDataAutoSyncContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts new file mode 100644 index 00000000000..ffbbcf5ebd5 --- /dev/null +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -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('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; + private readonly authTokenContext: IContextKey; + 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('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('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 { + 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 { + await this.authTokenService.deleteToken(); + } + + private async continueSync(): Promise { + // 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 { + 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())), + }); + } +} diff --git a/src/vs/workbench/services/authToken/electron-browser/authTokenService.ts b/src/vs/workbench/services/authToken/electron-browser/authTokenService.ts new file mode 100644 index 00000000000..b5a2b2d0c0a --- /dev/null +++ b/src/vs/workbench/services/authToken/electron-browser/authTokenService.ts @@ -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 = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService + ) { + super(); + this.channel = sharedProcessService.getChannel('authToken'); + this.channel.call('_getInitialStatus').then(status => { + this.updateStatus(status); + this._register(this.channel.listen('onDidChangeStatus')(status => this.updateStatus(status))); + }); + } + + getToken(): Promise { + return this.channel.call('getToken'); + } + + updateToken(token: string): Promise { + return this.channel.call('updateToken', [token]); + } + + refreshToken(): Promise { + return this.channel.call('getToken'); + } + + deleteToken(): Promise { + return this.channel.call('deleteToken'); + } + + private async updateStatus(status: AuthTokenStatus): Promise { + this._status = status; + this._onDidChangeStatus.fire(status); + } + +} + +registerSingleton(IAuthTokenService, AuthTokenService); diff --git a/src/vs/workbench/services/credentials/browser/credentialsService.ts b/src/vs/workbench/services/credentials/browser/credentialsService.ts index e005f8bc302..20946f64ae1 100644 --- a/src/vs/workbench/services/credentials/browser/credentialsService.ts +++ b/src/vs/workbench/services/credentials/browser/credentialsService.ts @@ -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'; diff --git a/src/vs/workbench/services/credentials/node/credentialsService.ts b/src/vs/workbench/services/credentials/node/credentialsService.ts index 970a5f82034..8c876d5b759 100644 --- a/src/vs/workbench/services/credentials/node/credentialsService.ts +++ b/src/vs/workbench/services/credentials/node/credentialsService.ts @@ -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'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 440d4834ffb..d89f16cc5a4 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -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'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 1a65c82b0c4..6ac8789dd5b 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -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);