diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 7aef5c8c7b7..1e8351687cf 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -49,10 +49,10 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; -import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel, UserDataSyncStoreManagementServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; @@ -202,6 +202,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); + services.set(IUserDataSyncStoreManagementService, new SyncDescriptor(UserDataSyncStoreManagementService)); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); services.set(IUserDataSyncMachinesService, new SyncDescriptor(UserDataSyncMachinesService)); services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService)); @@ -237,6 +238,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService); server.registerChannel('userDataSyncAccount', authTokenChannel); + const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService); + const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService); + server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel); + const userDataSyncService = accessor.get(IUserDataSyncService); const userDataSyncChannel = new UserDataSyncChannel(server, userDataSyncService, logService); server.registerChannel('userDataSync', userDataSyncChannel); diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index b119d2b8048..040c869d94c 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -22,7 +22,12 @@ export interface IBuiltInExtension { readonly metadata: any; } -export type ConfigurationSyncStore = { url: string, authenticationProviders: IStringDictionary<{ scopes: string[] }> }; +export type ConfigurationSyncStore = { + url: string, + insidersUrl?: string, + stableUrl?: string, + authenticationProviders: IStringDictionary<{ scopes: string[] }> +}; export interface IProductConfiguration { readonly version: string; diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 806dd30191b..283488c3c3a 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -6,7 +6,7 @@ import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isPromiseCanceledError } from 'vs/base/common/errors'; @@ -16,6 +16,8 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { localize } from 'vs/nls'; import { toLocalISOString } from 'vs/base/common/date'; +import { URI } from 'vs/base/common/uri'; +import { isEqual } from 'vs/base/common/resources'; type AutoSyncClassification = { sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -41,7 +43,8 @@ export class UserDataAutoSyncEnablementService extends Disposable { constructor( @IStorageService protected readonly storageService: IStorageService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUserDataSyncStoreManagementService protected readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService ) { super(); this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); @@ -58,7 +61,7 @@ export class UserDataAutoSyncEnablementService extends Disposable { } canToggleEnablement(): boolean { - return this.environmentService.sync === undefined; + return this.userDataSyncStoreManagementService.userDataSyncStore !== undefined && this.environmentService.sync === undefined; } private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void { @@ -83,7 +86,21 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i private readonly _onError: Emitter = this._register(new Emitter()); readonly onError: Event = this._onError.event; + private lastSyncUrl: URI | undefined; + private get syncUrl(): URI | undefined { + const value = this.storageService.get(storeUrlKey, StorageScope.GLOBAL); + return value ? URI.parse(value) : undefined; + } + private set syncUrl(syncUrl: URI | undefined) { + if (syncUrl) { + this.storageService.store(storeUrlKey, syncUrl.toString(), StorageScope.GLOBAL); + } else { + this.storageService.remove(storeUrlKey, StorageScope.GLOBAL); + } + } + constructor( + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -94,13 +111,12 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService ) { - super(storageService, environmentService); + super(storageService, environmentService, userDataSyncStoreManagementService); this.syncTriggerDelayer = this._register(new Delayer(0)); + this.lastSyncUrl = this.syncUrl; + this.syncUrl = userDataSyncStoreManagementService.userDataSyncStore?.url; - if (userDataSyncStoreService.userDataSyncStore) { - - storageService.store(storeUrlKey, userDataSyncStoreService.userDataSyncStore.url.toString(), StorageScope.GLOBAL); - + if (userDataSyncStoreManagementService.userDataSyncStore) { if (this.isEnabled()) { this.logService.info('Auto Sync is enabled.'); } else { @@ -123,7 +139,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i const { enabled, message } = this.isAutoSyncEnabled(); if (enabled) { if (this.autoSync.value === undefined) { - this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService); + this.autoSync.value = new AutoSync(this.lastSyncUrl, 1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreManagementService, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService); this.autoSync.value.register(this.autoSync.value.onDidStartSync(() => this.lastSyncTriggerTime = new Date().getTime())); this.autoSync.value.register(this.autoSync.value.onDidFinishSync(e => this.onDidFinishSync(e))); if (this.startAutoSync()) { @@ -164,6 +180,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i async turnOn(): Promise { this.stopDisableMachineEventually(); + this.lastSyncUrl = this.syncUrl; this.setEnablement(true); } @@ -228,7 +245,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } // Turned off from another device - if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) { + else if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) { await this.turnOff(false, true /* force soft turnoff on error */); this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud'); } @@ -261,6 +278,13 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this.logService.info('Auto Sync: Turned off sync because server has {0} content with older version than of client. Requires server reset.', userDataSyncError.resource); } + // Endpoint changed + else if (userDataSyncError.code === UserDataSyncErrorCode.ServiceChanged) { + await this.turnOff(false, true /* force soft turnoff on error */, true /* do not disable machine */); + await this.turnOn(); + this.logService.info('Auto Sync: Endpoint changed. Turned off auto sync, reset local state and turned on auto sync.'); + } + else { this.logService.error(userDataSyncError); this.successiveFailures++; @@ -342,7 +366,9 @@ class AutoSync extends Disposable { private syncPromise: CancelablePromise | undefined; constructor( + private readonly lastSyncUrl: URI | undefined, private readonly interval: number /* in milliseconds */, + private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, private readonly userDataSyncStoreService: IUserDataSyncStoreService, private readonly userDataSyncService: IUserDataSyncService, private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, @@ -407,14 +433,24 @@ class AutoSync extends Disposable { // Server has no data but this machine was synced before if (manifest === null && await this.userDataSyncService.hasPreviouslySynced()) { - // Sync was turned off in the cloud - throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); + if (!this.lastSyncUrl || isEqual(this.lastSyncUrl, this.userDataSyncStoreManagementService.userDataSyncStore?.url)) { + // Sync was turned off in the cloud + throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); + } else { + // Sync endpoint changed + throw new UserDataAutoSyncError(localize('endpoint changed', "Cannot sync because sync endpoint is changed"), UserDataSyncErrorCode.ServiceChanged); + } } const sessionId = this.storageService.get(sessionIdKey, StorageScope.GLOBAL); // Server session is different from client session if (sessionId && manifest && sessionId !== manifest.session) { - throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired); + if (!this.lastSyncUrl || isEqual(this.lastSyncUrl, this.userDataSyncStoreManagementService.userDataSyncStore?.url)) { + throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired); + } else { + // Sync endpoint changed + throw new UserDataAutoSyncError(localize('endpoint changed', "Cannot sync because sync endpoint is changed"), UserDataSyncErrorCode.ServiceChanged); + } } const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 70874ae2b1d..582cbd8b18d 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -13,13 +13,11 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ILogService } from 'vs/platform/log/common/log'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; import { joinPath, isEqualOrParent } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService'; import { distinct } from 'vs/base/common/arrays'; import { isArray, isString, isObject } from 'vs/base/common/types'; import { IHeaders } from 'vs/base/parts/request/common/request'; @@ -110,8 +108,11 @@ export interface IUserData { export type IAuthenticationProvider = { id: string, scopes: string[] }; export interface IUserDataSyncStore { - url: URI; - authenticationProviders: IAuthenticationProvider[]; + readonly url: URI; + readonly defaultUrl: URI; + readonly stableUrl: URI | undefined; + readonly insidersUrl: URI | undefined; + readonly authenticationProviders: IAuthenticationProvider[]; } export function isAuthenticationProvider(thing: any): thing is IAuthenticationProvider { @@ -121,27 +122,6 @@ export function isAuthenticationProvider(thing: any): thing is IAuthenticationPr && isArray(thing.scopes); } -export function getUserDataSyncStore(productService: IProductService, configurationService: IConfigurationService): IUserDataSyncStore | undefined { - const value = { - ...(productService[CONFIGURATION_SYNC_STORE_KEY] || {}), - ...(configurationService.getValue(CONFIGURATION_SYNC_STORE_KEY) || {}) - }; - if (value - && isString(value.url) - && isObject(value.authenticationProviders) - && Object.keys(value.authenticationProviders).every(authenticationProviderId => isArray(value.authenticationProviders[authenticationProviderId].scopes)) - ) { - return { - url: joinPath(URI.parse(value.url), 'v1'), - authenticationProviders: Object.keys(value.authenticationProviders).reduce((result, id) => { - result.push({ id, scopes: value.authenticationProviders[id].scopes }); - return result; - }, []) - }; - } - return undefined; -} - export const enum SyncResource { Settings = 'settings', Keybindings = 'keybindings', @@ -161,11 +141,20 @@ export interface IResourceRefHandle { created: number; } -export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); export type ServerResource = SyncResource | 'machines'; -export interface IUserDataSyncStoreService { +export type UserDataSyncStoreType = 'insiders' | 'stable'; + +export const IUserDataSyncStoreManagementService = createDecorator('IUserDataSyncStoreManagementService'); +export interface IUserDataSyncStoreManagementService { readonly _serviceBrand: undefined; readonly userDataSyncStore: IUserDataSyncStore | undefined; + switch(type: UserDataSyncStoreType): Promise; + getPreviousUserDataSyncStore(): Promise; +} + +export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); +export interface IUserDataSyncStoreService { + readonly _serviceBrand: undefined; readonly onDidChangeDonotMakeRequestsUntil: Event; readonly donotMakeRequestsUntil: Date | undefined; @@ -220,6 +209,7 @@ export enum UserDataSyncErrorCode { NoRef = 'NoRef', TurnedOff = 'TurnedOff', SessionExpired = 'SessionExpired', + ServiceChanged = 'ServiceChanged', LocalTooManyRequests = 'LocalTooManyRequests', LocalPreconditionFailed = 'LocalPreconditionFailed', LocalInvalidContent = 'LocalInvalidContent', diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 8ef3bb5daeb..934b64ec000 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -5,7 +5,7 @@ import { IServerChannel, IChannel, IPCServer } from 'vs/base/parts/ipc/common/ipc'; import { Event, Emitter } from 'vs/base/common/event'; -import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; @@ -260,3 +260,18 @@ export class UserDataSyncAccountServiceChannel implements IServerChannel { } } +export class UserDataSyncStoreManagementServiceChannel implements IServerChannel { + constructor(private readonly service: IUserDataSyncStoreManagementService) { } + + listen(_: unknown, event: string): Event { + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'switch': return this.service.switch(args[0]); + case 'getPreviousUserDataSyncStore': return this.service.getPreviousUserDataSyncStore(); + } + throw new Error('Invalid call'); + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index ad4258b110c..c05c868d1ff 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -5,7 +5,7 @@ import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, - UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID, MergeState, Change + UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID, MergeState, Change, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -76,6 +76,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ constructor( @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -90,7 +91,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser]; this.updateStatus(); - if (this.userDataSyncStoreService.userDataSyncStore) { + if (this.userDataSyncStoreManagementService.userDataSyncStore) { this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus())); this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeConflicts, () => undefined)))(() => this.updateConflicts())); } @@ -335,7 +336,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private computeStatus(): SyncStatus { - if (!this.userDataSyncStoreService.userDataSyncStore) { + if (!this.userDataSyncStoreManagementService.userDataSyncStore) { return SyncStatus.Uninitialized; } if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) { @@ -385,7 +386,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private async checkEnablement(): Promise { - if (!this.userDataSyncStoreService.userDataSyncStore) { + if (!this.userDataSyncStoreManagementService.userDataSyncStore) { throw new Error('Not enabled'); } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 2c0e43d4a5d..087369b95db 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService'; import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; @@ -20,18 +20,112 @@ import { generateUuid } from 'vs/base/common/uuid'; import { isWeb } from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; import { createCancelablePromise, timeout, CancelablePromise } from 'vs/base/common/async'; +import { isString, isObject, isArray } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +const SYNC_SERVICE_URL_TYPE = 'sync.store.url.type'; +const SYNC_PREVIOUS_STORE = 'sync.previous.store'; const DONOT_MAKE_REQUESTS_UNTIL_KEY = 'sync.donot-make-requests-until'; const USER_SESSION_ID_KEY = 'sync.user-session-id'; const MACHINE_SESSION_ID_KEY = 'sync.machine-session-id'; const REQUEST_SESSION_LIMIT = 100; const REQUEST_SESSION_INTERVAL = 1000 * 60 * 5; /* 5 minutes */ +type UserDataSyncStore = IUserDataSyncStore & { defaultType?: UserDataSyncStoreType; type?: UserDataSyncStoreType }; + +export abstract class AbstractUserDataSyncStoreManagementService extends Disposable implements IUserDataSyncStoreManagementService { + + _serviceBrand: any; + + readonly userDataSyncStore: UserDataSyncStore | undefined; + + constructor( + @IProductService protected readonly productService: IProductService, + @IConfigurationService protected readonly configurationService: IConfigurationService, + @IStorageService protected readonly storageService: IStorageService, + ) { + super(); + this.userDataSyncStore = this.toUserDataSyncStore(this.productService[CONFIGURATION_SYNC_STORE_KEY]); + } + + protected toUserDataSyncStore(syncStore: ConfigurationSyncStore | undefined): UserDataSyncStore | undefined { + if (syncStore) { + if (syncStore + && isString(syncStore.url) + && isObject(syncStore.authenticationProviders) + && Object.keys(syncStore.authenticationProviders).every(authenticationProviderId => isArray(syncStore.authenticationProviders[authenticationProviderId].scopes)) + ) { + const type: UserDataSyncStoreType | undefined = this.storageService.get(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL) as UserDataSyncStoreType | undefined; + const url = this.configurationService.getValue(CONFIGURATION_SYNC_STORE_KEY)?.url + || (type === 'insiders' ? syncStore.insidersUrl : type === 'stable' ? syncStore.stableUrl : undefined) + || syncStore.url; + return { + url: URI.parse(url), + type, + defaultType: syncStore.url === syncStore.insidersUrl ? 'insiders' : syncStore.url === syncStore.stableUrl ? 'stable' : undefined, + defaultUrl: URI.parse(syncStore.url), + stableUrl: syncStore.stableUrl ? URI.parse(syncStore.stableUrl) : undefined, + insidersUrl: syncStore.insidersUrl ? URI.parse(syncStore.insidersUrl) : undefined, + authenticationProviders: Object.keys(syncStore.authenticationProviders).reduce((result, id) => { + result.push({ id, scopes: syncStore.authenticationProviders[id].scopes }); + return result; + }, []) + }; + } + } + return undefined; + } + + abstract switch(type: UserDataSyncStoreType): Promise; + abstract getPreviousUserDataSyncStore(): Promise; + +} + +export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { + + private readonly previousConfigurationSyncStore: ConfigurationSyncStore | undefined; + + constructor( + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, + ) { + super(productService, configurationService, storageService); + + const previousConfigurationSyncStore = this.storageService.get(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL); + if (previousConfigurationSyncStore) { + this.previousConfigurationSyncStore = JSON.parse(previousConfigurationSyncStore); + } + + const syncStore = this.productService[CONFIGURATION_SYNC_STORE_KEY]; + if (syncStore) { + this.storageService.store(SYNC_PREVIOUS_STORE, JSON.stringify(syncStore), StorageScope.GLOBAL); + } else { + this.storageService.remove(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL); + } + } + + async switch(type: UserDataSyncStoreType): Promise { + if (type !== this.userDataSyncStore?.type) { + if (type === this.userDataSyncStore?.defaultType) { + this.storageService.remove(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL); + } else { + this.storageService.store(SYNC_SERVICE_URL_TYPE, type, StorageScope.GLOBAL); + } + } + } + + async getPreviousUserDataSyncStore(): Promise { + return this.toUserDataSyncStore(this.previousConfigurationSyncStore); + } +} + export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService { _serviceBrand: any; - readonly userDataSyncStore: IUserDataSyncStore | undefined; + private readonly userDataSyncStoreUrl: URI | undefined; + private authToken: { token: string, type: string } | undefined; private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>; private readonly session: RequestsSession; @@ -49,15 +143,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn constructor( @IProductService productService: IProductService, - @IConfigurationService configurationService: IConfigurationService, @IRequestService private readonly requestService: IRequestService, + @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, @IStorageService private readonly storageService: IStorageService, ) { super(); - this.userDataSyncStore = getUserDataSyncStore(productService, configurationService); + this.userDataSyncStoreUrl = this.userDataSyncStoreManagementService.userDataSyncStore ? joinPath(this.userDataSyncStoreManagementService.userDataSyncStore.url, 'v1') : undefined; this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService) .then(uuid => { const headers: IHeaders = { @@ -109,11 +203,11 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async getAllRefs(resource: ServerResource): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const uri = joinPath(this.userDataSyncStore.url, 'resource', resource); + const uri = joinPath(this.userDataSyncStoreUrl, 'resource', resource); const headers: IHeaders = {}; const context = await this.request({ type: 'GET', url: uri.toString(), headers }, CancellationToken.None); @@ -127,11 +221,11 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async resolveContent(resource: ServerResource, ref: string): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', resource, ref).toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, ref).toString(); const headers: IHeaders = {}; headers['Cache-Control'] = 'no-cache'; @@ -146,11 +240,11 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async delete(resource: ServerResource): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString(); const headers: IHeaders = {}; const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None); @@ -161,11 +255,11 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async read(resource: ServerResource, oldValue: IUserData | null, headers: IHeaders = {}): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', resource, 'latest').toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, 'latest').toString(); headers = { ...headers }; // Disable caching as they are cached by synchronisers headers['Cache-Control'] = 'no-cache'; @@ -193,11 +287,11 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async write(resource: ServerResource, data: string, ref: string | null, headers: IHeaders = {}): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString(); headers = { ...headers }; headers['Content-Type'] = 'text/plain'; if (ref) { @@ -218,11 +312,11 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async manifest(headers: IHeaders = {}): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'manifest').toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'manifest').toString(); headers = { ...headers }; headers['Content-Type'] = 'application/json'; @@ -253,11 +347,11 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } async clear(): Promise { - if (!this.userDataSyncStore) { + if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource').toString(); + const url = joinPath(this.userDataSyncStoreUrl, 'resource').toString(); const headers: IHeaders = { 'Content-Type': 'text/plain' }; const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None); diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts index 7868c0ebc2e..81483659e9b 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; @@ -16,6 +16,7 @@ import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/us export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { constructor( + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncService userDataSyncService: IUserDataSyncService, @@ -27,7 +28,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService, ) { - super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); + super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); this._register(Event.debounce(Event.any( Event.map(electronService.onWindowFocus, () => 'windowFocus'), diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index d70b8bb77cd..767fbbf6cb5 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -6,14 +6,14 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { generateUuid } from 'vs/base/common/uuid'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NullLogService, ILogService } from 'vs/platform/log/common/log'; -import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IFileService } from 'vs/platform/files/common/files'; @@ -87,6 +87,7 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IUserDataSyncLogService, logService); this.instantiationService.stub(ITelemetryService, NullTelemetryService); + this.instantiationService.stub(IUserDataSyncStoreManagementService, this.instantiationService.createInstance(UserDataSyncStoreManagementService)); this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService)); const userDataSyncAccountService: IUserDataSyncAccountService = this.instantiationService.createInstance(UserDataSyncAccountService); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts index b68f3a102a1..9b326f949d5 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -18,6 +18,7 @@ import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/us export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { constructor( + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncService userDataSyncService: IUserDataSyncService, @@ -30,7 +31,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService, ) { - super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); + super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); this._register(Event.debounce(Event.any( Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 943952bc67b..2e6e4b508cb 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -30,7 +30,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncResourceEnablementService, - getSyncResourceFromLocalPreview, IResourcePreview + getSyncResourceFromLocalPreview, IResourcePreview, IUserDataSyncStoreManagementService, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -54,6 +54,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { ViewContainerLocation, IViewContainersRegistry, Extensions, ViewContainer } from 'vs/workbench/common/views'; import { UserDataSyncViewPaneContainer, UserDataSyncDataViews } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncViews'; import { IUserDataSyncWorkbenchService, getSyncAreaLabel, AccountStatus, CONTEXT_SYNC_STATE, CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE, CONFIGURE_SYNC_COMMAND_ID, SHOW_SYNC_LOG_COMMAND_ID, SYNC_VIEW_CONTAINER_ID, SYNC_TITLE } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { isNative } from 'vs/base/common/platform'; const CONTEXT_CONFLICTS_SOURCES = new RawContextKey('conflictsSources', ''); @@ -118,6 +119,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IStorageService private readonly storageService: IStorageService, @IOpenerService private readonly openerService: IOpenerService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, ) { super(); @@ -270,7 +272,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private onAutoSyncError(error: UserDataSyncError): void { + private async onAutoSyncError(error: UserDataSyncError): Promise { switch (error.code) { case UserDataSyncErrorCode.SessionExpired: this.notificationService.notify({ @@ -319,6 +321,21 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } }); return; + case UserDataSyncErrorCode.ServiceChanged: + const current = this.userDataSyncStoreManagementService.userDataSyncStore; + const previous = await this.userDataSyncStoreManagementService.getPreviousUserDataSyncStore(); + // check if defaults changed + if (current && previous && + (!isEqual(current.defaultUrl, previous.defaultUrl) || + !isEqual(current.insidersUrl, previous.insidersUrl) || + !isEqual(current.stableUrl, previous.stableUrl)) + ) { + this.notificationService.notify({ + severity: Severity.Info, + message: localize('default url has changed', "Settings sync is restarted because {0} is switched to new service endpoint.", this.productService.nameLong), + }); + } + return; } } @@ -659,6 +676,54 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo return this.outputService.showChannel(Constants.userDataSyncLogChannelId); } + private async switchSyncService(): Promise { + const userDataSyncStore = this.userDataSyncStoreManagementService.userDataSyncStore; + if (userDataSyncStore?.insidersUrl && userDataSyncStore?.stableUrl && ![userDataSyncStore.insidersUrl, userDataSyncStore.stableUrl].includes(userDataSyncStore.url)) { + return new Promise((c, e) => { + const disposables: DisposableStore = new DisposableStore(); + const quickPick = disposables.add(this.quickInputService.createQuickPick<{ id: UserDataSyncStoreType, label: string, description?: string }>()); + quickPick.title = localize('switchSyncService.title', "Switch Settings Sync Service..."); + quickPick.placeholder = localize('choose sync service', "Choose settings sync Service to use"); + quickPick.description = isNative ? + localize('choose sync service description', "Switching settings sync service requires restarting {0}", this.productService.nameLong) : + localize('choose sync service description web', "Switching settings sync service requires reloading {0}", this.productService.nameLong); + quickPick.hideInput = true; + const getDescription = (url: URI): string | undefined => { + const isCurrent = isEqual(url, userDataSyncStore.url); + const isDefault = isEqual(url, userDataSyncStore.defaultUrl); + if (isCurrent && isDefault) { + return localize('default and current', "Default & Current"); + } + if (isDefault) { + return localize('default', "Default"); + } + if (isCurrent) { + return localize('current', "Current"); + } + return undefined; + }; + quickPick.items = [ + { + id: 'insiders', + label: localize('insiders', "Insiders"), + description: getDescription(userDataSyncStore.insidersUrl!) + }, + { + id: 'stable', + label: localize('stable', "Stable"), + description: getDescription(userDataSyncStore.stableUrl!) + } + ]; + disposables.add(quickPick.onDidAccept(() => { + this.userDataSyncWorkbenchService.switchSyncService(quickPick.selectedItems[0].id); + quickPick.hide(); + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + }); + } + } + private registerActions(): void { if (this.userDataAutoSyncService.canToggleEnablement()) { this.registerTurnOnSyncAction(); @@ -675,6 +740,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerSyncNowAction(); this.registerConfigureSyncAction(); this.registerShowSettingsAction(); + this.registerSwitchSyncServiceAction(); this.registerShowLogAction(); } @@ -1053,6 +1119,28 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo })); } + private registerSwitchSyncServiceAction(): void { + const that = this; + const userDataSyncStore = this.userDataSyncStoreManagementService.userDataSyncStore; + if (userDataSyncStore?.insidersUrl && userDataSyncStore?.stableUrl && ![userDataSyncStore.insidersUrl, userDataSyncStore.stableUrl].includes(userDataSyncStore.url)) { + this._register(registerAction2(class ShowSyncSettingsAction extends Action2 { + constructor() { + super({ + id: 'workbench.userDataSync.actions.switchSyncService', + title: { value: localize('workbench.userDataSync.actions.switchSyncService', "{0}: Switch Settings Sync Service...", SYNC_TITLE), original: 'Settings Sync: Switch Settings Sync Service...' }, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)), + }, + }); + } + run(accessor: ServicesAccessor): any { + return that.switchSyncService(); + } + })); + } + } + private registerViews(): void { const container = this.registerViewContainer(); this.registerDataViews(container); diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts index daef57314c0..35fde76ad97 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataAutoSyncService, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataAutoSyncService, UserDataSyncError, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; @@ -23,10 +23,11 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i constructor( @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IInstantiationService instantiationService: IInstantiationService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { - super(storageService, environmentService); + super(storageService, environmentService, userDataSyncStoreManagementService); this.channel = sharedProcessService.getChannel('userDataAutoSync'); this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => this.triggerSync([source], true))); } diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts new file mode 100644 index 00000000000..4a34eee9101 --- /dev/null +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { AbstractUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { URI } from 'vs/base/common/uri'; + +export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { + + private readonly channel: IChannel; + + constructor( + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + super(productService, configurationService, storageService); + this.channel = sharedProcessService.getChannel('userDataSyncStoreManagement'); + } + + async switch(type: UserDataSyncStoreType): Promise { + return this.channel.call('switch', [type]); + } + + async getPreviousUserDataSyncStore(): Promise { + const userDataSyncStore = await this.channel.call('getPreviousUserDataSyncStore'); + return this.revive(userDataSyncStore); + } + + private revive(userDataSyncStore: IUserDataSyncStore): IUserDataSyncStore { + return { + url: URI.revive(userDataSyncStore.url), + defaultUrl: URI.revive(userDataSyncStore.defaultUrl), + insidersUrl: URI.revive(userDataSyncStore.insidersUrl), + stableUrl: URI.revive(userDataSyncStore.stableUrl), + authenticationProviders: userDataSyncStore.authenticationProviders, + }; + } + +} diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 88aa0597e56..ac990654b16 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IAuthenticationProvider, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask, IUserDataSyncStoreManagementService, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, CONTEXT_ENABLE_SYNC_MERGES_VIEW, SYNC_MERGES_VIEW_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID, SYNC_TITLE } from 'vs/workbench/services/userDataSync/common/userDataSync'; @@ -30,6 +30,8 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IViewsService, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; +import { isNative } from 'vs/base/common/platform'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; type UserAccountClassification = { id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' }; @@ -97,7 +99,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, - @IProductService productService: IProductService, + @IProductService private readonly productService: IProductService, @IConfigurationService configurationService: IConfigurationService, @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @@ -107,9 +109,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat @IContextKeyService contextKeyService: IContextKeyService, @IViewsService private readonly viewsService: IViewsService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, + @IHostService private readonly hostService: IHostService, ) { super(); - this._authenticationProviders = getUserDataSyncStore(productService, configurationService)?.authenticationProviders || []; + this._authenticationProviders = this.userDataSyncStoreManagementService.userDataSyncStore?.authenticationProviders || []; this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService); this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService); @@ -383,6 +387,30 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat await this.viewsService.openViewContainer(SYNC_VIEW_CONTAINER_ID); } + async switchSyncService(type: UserDataSyncStoreType): Promise { + if (!this.userDataSyncStoreManagementService.userDataSyncStore + || !this.userDataSyncStoreManagementService.userDataSyncStore.insidersUrl + || !this.userDataSyncStoreManagementService.userDataSyncStore.stableUrl) { + return; + } + await this.userDataSyncStoreManagementService.switch(type); + const res = await this.dialogService.confirm({ + type: 'info', + message: isNative ? + localize('relaunchMessage', "Switching settings sync service requires a restart to take effect.") : + localize('relaunchMessageWeb', "Switching settings sync service requires a reload to take effect."), + detail: isNative ? + localize('relaunchDetail', "Press the restart button to restart {0} and switch.", this.productService.nameLong) : + localize('relaunchDetailWeb', "Press the reload button to reload {0} and switch.", this.productService.nameLong), + primaryButton: isNative ? + localize('restart', "&&Restart") : + localize('restartWeb', "&&Reload"), + }); + if (res.confirmed) { + this.hostService.restart(); + } + } + private async waitForActiveSyncViews(): Promise { const viewContainer = this.viewDescriptorService.getViewContainerById(SYNC_VIEW_CONTAINER_ID); if (viewContainer) { diff --git a/src/vs/workbench/services/userDataSync/common/userDataSync.ts b/src/vs/workbench/services/userDataSync/common/userDataSync.ts index 4e0e54f6252..aaaad9acf40 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSync.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IAuthenticationProvider, SyncStatus, SyncResource, Change, MergeState } from 'vs/platform/userDataSync/common/userDataSync'; +import { IAuthenticationProvider, SyncStatus, SyncResource, Change, MergeState, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; @@ -58,6 +58,7 @@ export interface IUserDataSyncWorkbenchService { turnOn(): Promise; turnoff(everyWhere: boolean): Promise; signIn(): Promise; + switchSyncService(type: UserDataSyncStoreType): Promise; resetSyncedData(): Promise; showSyncActivity(): Promise; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 000a505240b..d52fd0e9c7a 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -63,7 +63,7 @@ import 'vs/workbench/services/experiment/electron-browser/experimentService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; -import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataAutoSyncService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataAutoSyncService } from 'vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { TunnelService } from 'vs/platform/remote/node/tunnelService'; @@ -71,6 +71,7 @@ import { ITimerService } from 'vs/workbench/services/timer/browser/timerService' import { TimerService } from 'vs/workbench/services/timer/electron-browser/timerService'; registerSingleton(ICredentialsService, KeytarCredentialsService, true); +registerSingleton(IUserDataSyncStoreManagementService, UserDataSyncStoreManagementService); registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ITunnelService, TunnelService); registerSingleton(ITimerService, TimerService); @@ -134,5 +135,6 @@ import 'vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribu // Configuration Exporter import 'vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.contribution'; +import { UserDataSyncStoreManagementService } from 'vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService'; //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index d97eb6e2018..0669178db4c 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -65,10 +65,10 @@ import { ITunnelService, TunnelService } from 'vs/platform/remote/common/tunnel' import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; -import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; import { StorageKeysSyncRegistryService, IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; -import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; @@ -85,6 +85,7 @@ registerSingleton(IContextMenuService, ContextMenuService); registerSingleton(ITunnelService, TunnelService, true); registerSingleton(ILoggerService, FileLoggerService); registerSingleton(IUserDataSyncLogService, UserDataSyncLogService); +registerSingleton(IUserDataSyncStoreManagementService, UserDataSyncStoreManagementService); registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); registerSingleton(IUserDataSyncMachinesService, UserDataSyncMachinesService); registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService);