/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. 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'; import { IExtensionIdentifier, EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, allSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { localize } from 'vs/nls'; 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 { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; import { joinPath, isEqualOrParent, IExtUri } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; 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'; export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; export function getDisallowedIgnoredSettings(): string[] { const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); return Object.keys(allSettings).filter(setting => !!allSettings[setting].disallowSyncIgnore); } export function getDefaultIgnoredSettings(): string[] { const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); const ignoreSyncSettings = Object.keys(allSettings).filter(setting => !!allSettings[setting].ignoreSync); const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE); const disallowedSettings = getDisallowedIgnoredSettings(); return distinct([CONFIGURATION_SYNC_STORE_KEY, ...ignoreSyncSettings, ...machineSettings, ...disallowedSettings]); } export function registerConfiguration(): IDisposable { const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ id: 'settingsSync', order: 30, title: localize('settings sync', "Settings Sync"), type: 'object', properties: { 'settingsSync.keybindingsPerPlatform': { type: 'boolean', description: localize('settingsSync.keybindingsPerPlatform', "Synchronize keybindings for each platform."), default: true, scope: ConfigurationScope.APPLICATION, tags: ['sync', 'usesOnlineServices'] }, 'settingsSync.ignoredExtensions': { 'type': 'array', markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."), items: [{ type: 'string', pattern: EXTENSION_IDENTIFIER_PATTERN, errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") }], 'default': [], 'scope': ConfigurationScope.APPLICATION, uniqueItems: true, disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] }, 'settingsSync.ignoredSettings': { 'type': 'array', description: localize('settingsSync.ignoredSettings', "Configure settings to be ignored while synchronizing."), 'default': [], 'scope': ConfigurationScope.APPLICATION, $ref: ignoredSettingsSchemaId, additionalProperties: true, uniqueItems: true, disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] } } }); const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); const registerIgnoredSettingsSchema = () => { const disallowedIgnoredSettings = getDisallowedIgnoredSettings(); const defaultIgnoredSettings = getDefaultIgnoredSettings().filter(s => s !== CONFIGURATION_SYNC_STORE_KEY); const settings = Object.keys(allSettings.properties).filter(setting => defaultIgnoredSettings.indexOf(setting) === -1); const ignoredSettings = defaultIgnoredSettings.filter(setting => disallowedIgnoredSettings.indexOf(setting) === -1); const ignoredSettingsSchema: IJSONSchema = { items: { type: 'string', enum: [...settings, ...ignoredSettings.map(setting => `-${setting}`)] }, }; jsonRegistry.registerSchema(ignoredSettingsSchemaId, ignoredSettingsSchema); }; return configurationRegistry.onDidUpdateConfiguration(() => registerIgnoredSettingsSchema()); } // #region User Data Sync Store export interface IUserData { ref: string; content: string | null; } export type IAuthenticationProvider = { id: string, scopes: string[] }; export interface IUserDataSyncStore { readonly url: URI; readonly type: UserDataSyncStoreType; readonly defaultUrl: URI; readonly stableUrl: URI; readonly insidersUrl: URI; readonly canSwitch: boolean; readonly authenticationProviders: IAuthenticationProvider[]; } export function isAuthenticationProvider(thing: any): thing is IAuthenticationProvider { return thing && isObject(thing) && isString(thing.id) && isArray(thing.scopes); } export const enum SyncResource { Settings = 'settings', Keybindings = 'keybindings', Snippets = 'snippets', Extensions = 'extensions', GlobalState = 'globalState' } export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState]; export function getLastSyncResourceUri(syncResource: SyncResource, environmentService: IEnvironmentService, extUri: IExtUri): URI { return extUri.joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`); } export interface IUserDataManifest { latest?: Record session: string; } export interface IResourceRefHandle { ref: string; created: number; } export type ServerResource = SyncResource | 'machines'; export type UserDataSyncStoreType = 'insiders' | 'stable'; export const IUserDataSyncStoreManagementService = createDecorator('IUserDataSyncStoreManagementService'); export interface IUserDataSyncStoreManagementService { readonly _serviceBrand: undefined; readonly onDidChangeUserDataSyncStore: Event; readonly userDataSyncStore: IUserDataSyncStore | undefined; switch(type: UserDataSyncStoreType): Promise; getPreviousUserDataSyncStore(): Promise; } export interface IUserDataSyncStoreClient { readonly onDidChangeDonotMakeRequestsUntil: Event; readonly donotMakeRequestsUntil: Date | undefined; readonly onTokenFailed: Event; readonly onTokenSucceed: Event; setAuthToken(token: string, type: string): void; // Sync requests manifest(headers?: IHeaders): Promise; read(resource: ServerResource, oldValue: IUserData | null, headers?: IHeaders): Promise; write(resource: ServerResource, content: string, ref: string | null, headers?: IHeaders): Promise; clear(): Promise; delete(resource: ServerResource): Promise; getAllRefs(resource: ServerResource): Promise; resolveContent(resource: ServerResource, ref: string): Promise; } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); export interface IUserDataSyncStoreService extends IUserDataSyncStoreClient { readonly _serviceBrand: undefined; } export const IUserDataSyncBackupStoreService = createDecorator('IUserDataSyncBackupStoreService'); export interface IUserDataSyncBackupStoreService { readonly _serviceBrand: undefined; backup(resource: SyncResource, content: string): Promise; getAllRefs(resource: SyncResource): Promise; resolveContent(resource: SyncResource, ref?: string): Promise; } //#endregion // #region User Data Sync Headers export const HEADER_OPERATION_ID = 'x-operation-id'; export const HEADER_EXECUTION_ID = 'X-Execution-Id'; export function createSyncHeaders(executionId: string): IHeaders { const headers: IHeaders = {}; headers[HEADER_EXECUTION_ID] = executionId; return headers; } //#endregion // #region User Data Sync Error export enum UserDataSyncErrorCode { // Client Errors (>= 400 ) Unauthorized = 'Unauthorized', /* 401 */ Conflict = 'Conflict', /* 409 */ Gone = 'Gone', /* 410 */ PreconditionFailed = 'PreconditionFailed', /* 412 */ TooLarge = 'TooLarge', /* 413 */ UpgradeRequired = 'UpgradeRequired', /* 426 */ PreconditionRequired = 'PreconditionRequired', /* 428 */ TooManyRequests = 'RemoteTooManyRequests', /* 429 */ TooManyRequestsAndRetryAfter = 'TooManyRequestsAndRetryAfter', /* 429 + Retry-After */ // Local Errors RequestFailed = 'RequestFailed', RequestCanceled = 'RequestCanceled', RequestTimeout = 'RequestTimeout', NoRef = 'NoRef', TurnedOff = 'TurnedOff', SessionExpired = 'SessionExpired', ServiceChanged = 'ServiceChanged', DefaultServiceChanged = 'DefaultServiceChanged', LocalTooManyRequests = 'LocalTooManyRequests', LocalPreconditionFailed = 'LocalPreconditionFailed', LocalInvalidContent = 'LocalInvalidContent', LocalError = 'LocalError', IncompatibleLocalContent = 'IncompatibleLocalContent', IncompatibleRemoteContent = 'IncompatibleRemoteContent', UnresolvedConflicts = 'UnresolvedConflicts', Unknown = 'Unknown', } export class UserDataSyncError extends Error { constructor( message: string, readonly code: UserDataSyncErrorCode, readonly resource?: SyncResource, readonly operationId?: string ) { super(message); this.name = `${this.code} (UserDataSyncError) syncResource:${this.resource || 'unknown'} operationId:${this.operationId || 'unknown'}`; } } export class UserDataSyncStoreError extends UserDataSyncError { constructor(message: string, readonly url: string, code: UserDataSyncErrorCode, operationId: string | undefined) { super(message, code, undefined, operationId); } } export class UserDataAutoSyncError extends UserDataSyncError { constructor(message: string, code: UserDataSyncErrorCode) { super(message, code); } } export namespace UserDataSyncError { export function toUserDataSyncError(error: Error): UserDataSyncError { if (error instanceof UserDataSyncError) { return error; } const match = /^(.+) \(UserDataSyncError\) syncResource:(.+) operationId:(.+)$/.exec(error.name); if (match && match[1]) { const syncResource = match[2] === 'unknown' ? undefined : match[2] as SyncResource; const operationId = match[3] === 'unknown' ? undefined : match[3]; return new UserDataSyncError(error.message, match[1], syncResource, operationId); } return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown); } } //#endregion // #region User Data Synchroniser export interface ISyncExtension { identifier: IExtensionIdentifier; version?: string; disabled?: boolean; installed?: boolean; state?: IStringDictionary; } export interface ISyncExtensionWithVersion extends ISyncExtension { version: string; } export interface IStorageValue { version: number; value: string; } export interface IGlobalState { storage: IStringDictionary; } export const enum SyncStatus { Uninitialized = 'uninitialized', Idle = 'idle', Syncing = 'syncing', HasConflicts = 'hasConflicts', } export interface ISyncResourceHandle { created: number; uri: URI; } export interface IRemoteUserData { ref: string; syncData: ISyncData | null; } export interface ISyncData { version: number; machineId?: string; content: string; } export const enum Change { None, Added, Modified, Deleted, } export const enum MergeState { Preview = 'preview', Conflict = 'conflict', Accepted = 'accepted', } export interface IResourcePreview { readonly remoteResource: URI; readonly localResource: URI; readonly previewResource: URI; readonly acceptedResource: URI; readonly localChange: Change; readonly remoteChange: Change; readonly mergeState: MergeState; } export interface ISyncResourcePreview { readonly isLastSyncFromCurrentMachine: boolean; readonly resourcePreviews: IResourcePreview[]; } export interface IUserDataInitializer { initialize(userData: IUserData): Promise; } export interface IUserDataSynchroniser { readonly resource: SyncResource; readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly conflicts: IResourcePreview[]; readonly onDidChangeConflicts: Event; readonly onDidChangeLocal: Event; sync(manifest: IUserDataManifest | null, headers: IHeaders): Promise; replace(uri: URI): Promise; stop(): Promise; preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise; accept(resource: URI, content?: string | null): Promise; merge(resource: URI): Promise; discard(resource: URI): Promise; apply(force: boolean, headers: IHeaders): Promise; hasPreviouslySynced(): Promise; hasLocalData(): Promise; resetLocal(): Promise; resolveContent(resource: URI): Promise; getRemoteSyncResourceHandles(): Promise; getLocalSyncResourceHandles(): Promise; getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>; getMachineId(syncResourceHandle: ISyncResourceHandle): Promise; } //#endregion // #region keys synced only in web export const SYNC_SERVICE_URL_TYPE = 'sync.store.url.type'; export function getEnablementKey(resource: SyncResource) { return `sync.enable.${resource}`; } // #endregion // #region User Data Sync Services export const IUserDataSyncResourceEnablementService = createDecorator('IUserDataSyncResourceEnablementService'); export interface IUserDataSyncResourceEnablementService { _serviceBrand: any; readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]>; isResourceEnabled(resource: SyncResource): boolean; setResourceEnablement(resource: SyncResource, enabled: boolean): void; getResourceSyncStateVersion(resource: SyncResource): string | undefined; } export interface ISyncTask { readonly manifest: IUserDataManifest | null; run(): Promise; stop(): Promise; } export interface IManualSyncTask extends IDisposable { readonly id: string; readonly status: SyncStatus; readonly manifest: IUserDataManifest | null; readonly onSynchronizeResources: Event<[SyncResource, URI[]][]>; preview(): Promise<[SyncResource, ISyncResourcePreview][]>; accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]>; merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]>; discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]>; discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]>; apply(): Promise<[SyncResource, ISyncResourcePreview][]>; pull(): Promise; push(): Promise; stop(): Promise; } export const IUserDataSyncService = createDecorator('IUserDataSyncService'); export interface IUserDataSyncService { _serviceBrand: any; readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly conflicts: [SyncResource, IResourcePreview[]][]; readonly onDidChangeConflicts: Event<[SyncResource, IResourcePreview[]][]>; readonly onDidChangeLocal: Event; readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]>; readonly lastSyncTime: number | undefined; readonly onDidChangeLastSyncTime: Event; readonly onDidResetRemote: Event; readonly onDidResetLocal: Event; createSyncTask(disableCache?: boolean): Promise; createManualSyncTask(): Promise; replace(uri: URI): Promise; reset(): Promise; resetRemote(): Promise; resetLocal(): Promise; hasLocalData(): Promise; hasPreviouslySynced(): Promise; resolveContent(resource: URI): Promise; accept(resource: SyncResource, conflictResource: URI, content: string | null | undefined, apply: boolean): Promise; getLocalSyncResourceHandles(resource: SyncResource): Promise; getRemoteSyncResourceHandles(resource: SyncResource): Promise; getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>; getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise; } export const IUserDataAutoSyncEnablementService = createDecorator('IUserDataAutoSyncEnablementService'); export interface IUserDataAutoSyncEnablementService { _serviceBrand: any; readonly onDidChangeEnablement: Event; isEnabled(): boolean; canToggleEnablement(): boolean; } export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); export interface IUserDataAutoSyncService { _serviceBrand: any; readonly onError: Event; turnOn(): Promise; turnOff(everywhere: boolean): Promise; triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); export interface IUserDataSyncUtilService { readonly _serviceBrand: undefined; resolveUserBindings(userbindings: string[]): Promise>; resolveFormattingOptions(resource: URI): Promise; resolveDefaultIgnoredSettings(): Promise; } export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); export interface IUserDataSyncLogService extends ILogService { } export interface IConflictSetting { key: string; localValue: any | undefined; remoteValue: any | undefined; } //#endregion export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync'; export const PREVIEW_DIR_NAME = 'preview'; export function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined { if (localPreview.scheme === USER_DATA_SYNC_SCHEME) { return undefined; } localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme }); return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; }