/*--------------------------------------------------------------------------------------------- * 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'; 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 { 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'; 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 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, ...machineSettings, ...disallowedSettings]); } export function registerConfiguration(): IDisposable { const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings'; const ignoredExtensionsSchemaId = 'vscode://schemas/ignoredExtensions'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ id: 'sync', order: 30, title: localize('sync', "Sync"), type: 'object', properties: { 'sync.keybindingsPerPlatform': { type: 'boolean', description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."), default: true, scope: ConfigurationScope.APPLICATION, tags: ['sync', 'usesOnlineServices'] }, 'sync.ignoredExtensions': { 'type': 'array', 'description': localize('sync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."), $ref: ignoredExtensionsSchemaId, 'default': [], 'scope': ConfigurationScope.APPLICATION, uniqueItems: true, disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] }, 'sync.ignoredSettings': { 'type': 'array', description: localize('sync.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); }; jsonRegistry.registerSchema(ignoredExtensionsSchemaId, { type: 'string', pattern: EXTENSION_IDENTIFIER_PATTERN, errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") }); 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 { url: URI; authenticationProviders: IAuthenticationProvider[]; } export function isAuthenticationProvider(thing: any): thing is IAuthenticationProvider { return thing && isObject(thing) && isString(thing.id) && isArray(thing.scopes); } export function getUserDataSyncStore(productService: IProductService, configurationService: IConfigurationService): IUserDataSyncStore | undefined { const value = configurationService.getValue(CONFIGURATION_SYNC_STORE_KEY) || productService[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', Snippets = 'snippets', Extensions = 'extensions', GlobalState = 'globalState' } export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState]; export interface IUserDataManifest { latest?: Record session: string; } export interface IResourceRefHandle { ref: string; created: number; } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); export type ServerResource = SyncResource | 'machines'; export interface IUserDataSyncStoreService { _serviceBrand: undefined; readonly userDataSyncStore: IUserDataSyncStore | undefined; read(resource: ServerResource, oldValue: IUserData | null): Promise; write(resource: ServerResource, content: string, ref: string | null): Promise; manifest(): Promise; clear(): Promise; getAllRefs(resource: ServerResource): Promise; resolveContent(resource: ServerResource, ref: string): Promise; delete(resource: ServerResource): Promise; } export const IUserDataSyncBackupStoreService = createDecorator('IUserDataSyncBackupStoreService'); export interface IUserDataSyncBackupStoreService { _serviceBrand: undefined; backup(resource: SyncResource, content: string): Promise; getAllRefs(resource: SyncResource): Promise; resolveContent(resource: SyncResource, ref?: string): Promise; } //#endregion // #region User Data Sync Error export enum UserDataSyncErrorCode { // Client Errors (>= 400 ) Unauthorized = 'Unauthorized', /* 401 */ PreconditionFailed = 'PreconditionFailed', /* 412 */ TooLarge = 'TooLarge', /* 413 */ UpgradeRequired = 'UpgradeRequired', /* 426 */ PreconditionRequired = 'PreconditionRequired', /* 428 */ TooManyRequests = 'RemoteTooManyRequests', /* 429 */ // Local Errors ConnectionRefused = 'ConnectionRefused', NoRef = 'NoRef', TurnedOff = 'TurnedOff', SessionExpired = 'SessionExpired', LocalTooManyRequests = 'LocalTooManyRequests', LocalPreconditionFailed = 'LocalPreconditionFailed', LocalInvalidContent = 'LocalInvalidContent', LocalError = 'LocalError', Incompatible = 'Incompatible', Unknown = 'Unknown', } export class UserDataSyncError extends Error { constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly resource?: SyncResource) { super(message); this.name = `${this.code} (UserDataSyncError) ${this.resource}`; } static toUserDataSyncError(error: Error): UserDataSyncError { if (error instanceof UserDataSyncStoreError) { return error; } const match = /^(.+) \(UserDataSyncError\) (.+)?$/.exec(error.name); if (match && match[1]) { return new UserDataSyncError(error.message, match[1], match[2]); } return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown); } } export class UserDataSyncStoreError extends UserDataSyncError { constructor(message: string, code: UserDataSyncErrorCode) { super(message, code); } } //#endregion // #region User Data Synchroniser export interface ISyncExtension { identifier: IExtensionIdentifier; version?: string; disabled?: boolean; installed?: boolean; } 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 type Conflict = { remote: URI, local: URI }; export interface ISyncPreviewResult { readonly hasLocalChanged: boolean; readonly hasRemoteChanged: boolean; } export interface IUserDataSynchroniser { readonly resource: SyncResource; readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly conflicts: Conflict[]; readonly onDidChangeConflicts: Event; readonly onDidChangeLocal: Event; pull(): Promise; push(): Promise; sync(manifest: IUserDataManifest | null): Promise; replace(uri: URI): Promise; stop(): Promise; getSyncPreview(): Promise hasPreviouslySynced(): Promise hasLocalData(): Promise; resetLocal(): Promise; resolveContent(resource: URI): Promise; acceptConflict(conflictResource: URI, content: string): Promise; getRemoteSyncResourceHandles(): Promise; getLocalSyncResourceHandles(): Promise; getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>; getMachineId(syncResourceHandle: ISyncResourceHandle): Promise; } //#endregion // #region User Data Sync Services export const IUserDataSyncEnablementService = createDecorator('IUserDataSyncEnablementService'); export interface IUserDataSyncEnablementService { _serviceBrand: any; readonly onDidChangeEnablement: Event; readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]>; isEnabled(): boolean; setEnablement(enabled: boolean): void; canToggleEnablement(): boolean; isResourceEnabled(resource: SyncResource): boolean; setResourceEnablement(resource: SyncResource, enabled: boolean): void; } export type SyncResourceConflicts = { syncResource: SyncResource, conflicts: Conflict[] }; export const IUserDataSyncService = createDecorator('IUserDataSyncService'); export interface IUserDataSyncService { _serviceBrand: any; readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly conflicts: SyncResourceConflicts[]; readonly onDidChangeConflicts: Event; readonly onDidChangeLocal: Event; readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]>; readonly lastSyncTime: number | undefined; readonly onDidChangeLastSyncTime: Event; pull(): Promise; sync(): Promise; stop(): Promise; replace(uri: URI): Promise; reset(): Promise; resetLocal(): Promise; isFirstTimeSyncWithMerge(): Promise; resolveContent(resource: URI): Promise; acceptConflict(conflictResource: URI, content: string): 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 IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); export interface IUserDataAutoSyncService { _serviceBrand: any; readonly onError: Event; triggerAutoSync(sources: string[]): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); export interface IUserDataSyncUtilService { _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]; }