Merge from vscode 79a1f5a5ca0c6c53db617aa1fa5a2396d2caebe2

This commit is contained in:
ADS Merger
2020-05-31 19:47:51 +00:00
parent 84492049e8
commit 28be33cfea
913 changed files with 28242 additions and 15549 deletions

View File

@@ -7,10 +7,10 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
import { CancelablePromise } from 'vs/base/common/async';
import { CancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ParseError, parse } from 'vs/base/common/json';
@@ -21,6 +21,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { isString } from 'vs/base/common/types';
import { uppercaseFirstLetter } from 'vs/base/common/strings';
import { equals } from 'vs/base/common/arrays';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IStorageService } from 'vs/platform/storage/common/storage';
type SyncSourceClassification = {
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -33,19 +35,34 @@ export interface IRemoteUserData {
export interface ISyncData {
version: number;
machineId?: string;
content: string;
}
function isSyncData(thing: any): thing is ISyncData {
return thing
&& (thing.version && typeof thing.version === 'number')
&& (thing.content && typeof thing.content === 'string')
&& Object.keys(thing).length === 2;
if (thing
&& (thing.version !== undefined && typeof thing.version === 'number')
&& (thing.content !== undefined && typeof thing.content === 'string')) {
// backward compatibility
if (Object.keys(thing).length === 2) {
return true;
}
if (Object.keys(thing).length === 3
&& (thing.machineId !== undefined && typeof thing.machineId === 'string')) {
return true;
}
}
return false;
}
export abstract class AbstractSynchroniser extends Disposable {
protected readonly syncFolder: URI;
private readonly currentMachineIdPromise: Promise<string>;
private _status: SyncStatus = SyncStatus.Idle;
get status(): SyncStatus { return this._status; }
@@ -57,7 +74,8 @@ export abstract class AbstractSynchroniser extends Disposable {
private _onDidChangeConflicts: Emitter<Conflict[]> = this._register(new Emitter<Conflict[]>());
readonly onDidChangeConflicts: Event<Conflict[]> = this._onDidChangeConflicts.event;
protected readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
protected readonly lastSyncResource: URI;
@@ -67,10 +85,11 @@ export abstract class AbstractSynchroniser extends Disposable {
readonly resource: SyncResource,
@IFileService protected readonly fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ITelemetryService protected readonly telemetryService: ITelemetryService,
@IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
@IConfigurationService protected readonly configurationService: IConfigurationService,
) {
@@ -78,6 +97,22 @@ export abstract class AbstractSynchroniser extends Disposable {
this.syncResourceLogLabel = uppercaseFirstLetter(this.resource);
this.syncFolder = joinPath(environmentService.userDataSyncHome, resource);
this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resource}.json`);
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
}
protected async triggerLocalChange(): Promise<void> {
if (this.isEnabled()) {
this.localChangeTriggerScheduler.schedule();
}
}
protected async doTriggerLocalChange(): Promise<void> {
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
const lastSyncUserData = await this.getLastSyncUserData();
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData)).hasRemoteChanged : true;
if (hasRemoteChanged) {
this._onDidChangeLocal.fire();
}
}
protected setStatus(status: SyncStatus): void {
@@ -108,7 +143,7 @@ export abstract class AbstractSynchroniser extends Disposable {
protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resource); }
async sync(ref?: string): Promise<void> {
async sync(manifest: IUserDataManifest | null): Promise<void> {
if (!this.isEnabled()) {
if (this.status !== SyncStatus.Idle) {
await this.stop();
@@ -129,7 +164,7 @@ export abstract class AbstractSynchroniser extends Disposable {
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData);
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
let status: SyncStatus = SyncStatus.Idle;
try {
@@ -144,6 +179,51 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
async replace(uri: URI): Promise<boolean> {
const content = await this.resolveContent(uri);
if (!content) {
return false;
}
const syncData = this.parseSyncData(content);
if (!syncData) {
return false;
}
await this.stop();
try {
this.logService.trace(`${this.syncResourceLogLabel}: Started resetting ${this.resource.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
await this.performReplace(syncData, remoteUserData, lastSyncUserData);
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
return true;
}
private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
if (lastSyncUserData) {
const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined;
// Last time synced resource and latest resource on server are same
if (lastSyncUserData.ref === latestRef) {
return lastSyncUserData;
}
// There is no resource on server and last time it was synced with no resource
if (latestRef === undefined && lastSyncUserData.syncData === null) {
return lastSyncUserData;
}
}
return this.getRemoteUserData(lastSyncUserData);
}
async getSyncPreview(): Promise<ISyncPreviewResult> {
if (!this.isEnabled()) {
return { hasLocalChanged: false, hasRemoteChanged: false };
@@ -158,7 +238,7 @@ export abstract class AbstractSynchroniser extends Disposable {
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
// current version is not compatible with cloud version
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource });
throw new UserDataSyncError(localize('incompatible', "Cannot sync {0} as its version {1} is not compatible with cloud {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource);
throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource);
}
try {
const status = await this.performSync(remoteUserData, lastSyncUserData);
@@ -166,7 +246,7 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (e) {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.RemotePreconditionFailed:
case UserDataSyncErrorCode.PreconditionFailed:
// Rejected as there is a new remote version. Syncing again...
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`);
@@ -207,6 +287,18 @@ export abstract class AbstractSynchroniser extends Disposable {
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${this.resource}/${ref}` });
}
async getMachineId({ uri }: ISyncResourceHandle): Promise<string | undefined> {
const ref = basename(uri);
if (isEqual(uri, this.toRemoteBackupResource(ref))) {
const { content } = await this.getUserData(ref);
if (content) {
const syncData = this.parseSyncData(content);
return syncData?.machineId;
}
}
return undefined;
}
async resolveContent(uri: URI): Promise<string | null> {
const ref = basename(uri);
if (isEqual(uri, this.toRemoteBackupResource(ref))) {
@@ -225,18 +317,21 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (e) { /* ignore */ }
}
protected async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
try {
const content = await this.fileService.readFile(this.lastSyncResource);
const parsed = JSON.parse(content.value.toString());
let syncData: ISyncData = JSON.parse(parsed.content);
const userData: IUserData = parsed as IUserData;
if (userData.content === null) {
return { ref: parsed.ref, syncData: null } as T;
}
const syncData: ISyncData = JSON.parse(userData.content);
// Migration from old content to sync data
if (!isSyncData(syncData)) {
syncData = { version: this.version, content: parsed.content };
/* Check if syncData is of expected type. Return only if matches */
if (isSyncData(syncData)) {
return { ...parsed, ...{ syncData, content: undefined } };
}
return { ...parsed, ...{ syncData, content: undefined } };
} catch (error) {
if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) {
// log error always except when file does not exist
@@ -247,11 +342,11 @@ export abstract class AbstractSynchroniser extends Disposable {
}
protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary<any> = {}): Promise<void> {
const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: JSON.stringify(lastSyncRemoteUserData.syncData), ...additionalProps };
const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps };
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
}
protected async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise<IRemoteUserData> {
async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise<IRemoteUserData> {
const { ref, content } = await this.getUserData(lastSyncData);
let syncData: ISyncData | null = null;
if (content !== null) {
@@ -260,20 +355,16 @@ export abstract class AbstractSynchroniser extends Disposable {
return { ref, syncData };
}
protected parseSyncData(content: string): ISyncData | null {
let syncData: ISyncData | null = null;
protected parseSyncData(content: string): ISyncData {
try {
syncData = <ISyncData>JSON.parse(content);
// Migration from old content to sync data
if (!isSyncData(syncData)) {
syncData = { version: this.version, content };
const syncData: ISyncData = JSON.parse(content);
if (isSyncData(syncData)) {
return syncData;
}
} catch (e) {
this.logService.error(e);
} catch (error) {
this.logService.error(error);
}
return syncData;
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.Incompatible, this.resource);
}
private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> {
@@ -287,7 +378,8 @@ export abstract class AbstractSynchroniser extends Disposable {
}
protected async updateRemoteUserData(content: string, ref: string | null): Promise<IRemoteUserData> {
const syncData: ISyncData = { version: this.version, content };
const machineId = await this.currentMachineIdPromise;
const syncData: ISyncData = { version: this.version, machineId, content };
ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref);
return { ref, syncData };
}
@@ -301,6 +393,7 @@ export abstract class AbstractSynchroniser extends Disposable {
protected abstract readonly version: number;
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
protected abstract performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void>;
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult>;
}
@@ -321,6 +414,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
resource: SyncResource,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@@ -328,7 +422,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(resource, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
this._register(this.fileService.watch(dirname(file)));
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
}
@@ -403,7 +497,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
// Otherwise fire change event
else {
this._onDidChangeLocal.fire();
this.triggerLocalChange();
}
}
@@ -426,6 +520,7 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni
resource: SyncResource,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@@ -434,7 +529,7 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni
@IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(file, resource, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(file, resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
}
protected hasErrors(content: string): boolean {

View File

@@ -7,6 +7,10 @@ import { values, keys } from 'vs/base/common/map';
import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { startsWith } from 'vs/base/common/strings';
import { deepClone } from 'vs/base/common/objects';
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { distinct } from 'vs/base/common/arrays';
export interface IMergeResult {
added: ISyncExtension[];
@@ -21,16 +25,15 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
const updated: ISyncExtension[] = [];
if (!remoteExtensions) {
const remote = localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase()));
return {
added,
removed,
updated,
remote: localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase()))
remote: remote.length > 0 ? remote : null
};
}
// massage incoming extension - add disabled property
const massageIncomingExtension = (extension: ISyncExtension): ISyncExtension => ({ ...extension, ...{ disabled: !!extension.disabled } });
localExtensions = localExtensions.map(massageIncomingExtension);
remoteExtensions = remoteExtensions.map(massageIncomingExtension);
lastSyncExtensions = lastSyncExtensions ? lastSyncExtensions.map(massageIncomingExtension) : null;
@@ -53,7 +56,14 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
};
const localExtensionsMap = localExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce((map: Map<string, ISyncExtension>, extension: ISyncExtension) => {
const key = getKey(extension);
extension = deepClone(extension);
if (localExtensionsMap.get(key)?.installed) {
extension.installed = true;
}
return addExtensionToMap(map, extension);
}, new Map<string, ISyncExtension>());
const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => {
@@ -62,90 +72,82 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
}, new Set<string>());
const localToRemote = compare(localExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { added: [], removed: [], updated: [], remote: null };
}
if (localToRemote.added.size > 0 || localToRemote.removed.size > 0 || localToRemote.updated.size > 0) {
const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet);
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet);
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
// massage outgoing extension - remove disabled property
const massageOutgoingExtension = (extension: ISyncExtension, key: string): ISyncExtension => {
const massagedExtension: ISyncExtension = {
identifier: {
id: extension.identifier.id,
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
},
};
if (extension.disabled) {
massagedExtension.disabled = true;
}
if (extension.version) {
massagedExtension.version = extension.version;
}
return massagedExtension;
};
// Remotely removed extension.
for (const key of values(baseToRemote.removed)) {
const e = localExtensionsMap.get(key);
if (e) {
removed.push(e.identifier);
}
}
// Remotely added extension
for (const key of values(baseToRemote.added)) {
// Got added in local
if (baseToLocal.added.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
// Remotely removed extension.
for (const key of values(baseToRemote.removed)) {
const e = localExtensionsMap.get(key);
if (e) {
removed.push(e.identifier);
}
} else {
// Add to local
added.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
}
}
// Remotely updated extensions
for (const key of values(baseToRemote.updated)) {
// Update in local always
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
}
// Locally added extensions
for (const key of values(baseToLocal.added)) {
// Not there in remote
if (!baseToRemote.added.has(key)) {
newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!);
}
}
// Locally updated extensions
for (const key of values(baseToLocal.updated)) {
// If removed in remote
if (baseToRemote.removed.has(key)) {
continue;
}
// If not updated in remote
if (!baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!);
// Remotely added extension
for (const key of values(baseToRemote.added)) {
// Got added in local
if (baseToLocal.added.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
}
} else {
// Add only installed extension to local
const remoteExtension = remoteExtensionsMap.get(key)!;
if (remoteExtension.installed) {
added.push(massageOutgoingExtension(remoteExtension, key));
}
}
}
}
// Locally removed extensions
for (const key of values(baseToLocal.removed)) {
// If not skipped and not updated in remote
if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.delete(key);
// Remotely updated extensions
for (const key of values(baseToRemote.updated)) {
// Update in local always
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
}
// Locally added extensions
for (const key of values(baseToLocal.added)) {
// Not there in remote
if (!baseToRemote.added.has(key)) {
newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!);
}
}
// Locally updated extensions
for (const key of values(baseToLocal.updated)) {
// If removed in remote
if (baseToRemote.removed.has(key)) {
continue;
}
// If not updated in remote
if (!baseToRemote.updated.has(key)) {
const extension = deepClone(localExtensionsMap.get(key)!);
// Retain installed property
if (newRemoteExtensionsMap.get(key)?.installed) {
extension.installed = true;
}
newRemoteExtensionsMap.set(key, extension);
}
}
// Locally removed extensions
for (const key of values(baseToLocal.removed)) {
// If not skipped and not updated in remote
if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) {
// Remove only if it is an installed extension
if (lastSyncExtensionsMap?.get(key)?.installed) {
newRemoteExtensionsMap.delete(key);
}
}
}
}
const remote: ISyncExtension[] = [];
const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>());
const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>(), { checkInstalledProperty: true });
if (remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0) {
newRemoteExtensionsMap.forEach((value, key) => remote.push(massageOutgoingExtension(value, key)));
}
@@ -153,7 +155,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
return { added, removed, updated, remote: remote.length ? remote : null };
}
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>, { checkInstalledProperty }: { checkInstalledProperty: boolean } = { checkInstalledProperty: false }): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = from ? keys(from).filter(key => !ignoredExtensions.has(key)) : [];
const toKeys = keys(to).filter(key => !ignoredExtensions.has(key));
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
@@ -169,6 +171,7 @@ function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISync
if (!toExtension
|| fromExtension.disabled !== toExtension.disabled
|| fromExtension.version !== toExtension.version
|| (checkInstalledProperty && fromExtension.installed !== toExtension.installed)
) {
updated.add(key);
}
@@ -176,3 +179,44 @@ function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISync
return { added, removed, updated };
}
// massage incoming extension - add optional properties
function massageIncomingExtension(extension: ISyncExtension): ISyncExtension {
return { ...extension, ...{ disabled: !!extension.disabled, installed: !!extension.installed } };
}
// massage outgoing extension - remove optional properties
function massageOutgoingExtension(extension: ISyncExtension, key: string): ISyncExtension {
const massagedExtension: ISyncExtension = {
identifier: {
id: extension.identifier.id,
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
},
};
if (extension.disabled) {
massagedExtension.disabled = true;
}
if (extension.installed) {
massagedExtension.installed = true;
}
if (extension.version) {
massagedExtension.version = extension.version;
}
return massagedExtension;
}
export function getIgnoredExtensions(installed: ILocalExtension[], configurationService: IConfigurationService): string[] {
const defaultIgnoredExtensions = installed.filter(i => i.isMachineScoped).map(i => i.identifier.id.toLowerCase());
const value = (configurationService.getValue<string[]>('sync.ignoredExtensions') || []).map(id => id.toLowerCase());
const added: string[] = [], removed: string[] = [];
if (Array.isArray(value)) {
for (const key of value) {
if (startsWith(key, '-')) {
removed.push(key.substring(1));
} else {
added.push(key);
}
}
}
return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1));
}

View File

@@ -3,22 +3,24 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IFileService } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { joinPath, dirname, basename } from 'vs/base/common/resources';
import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
import { compare } from 'vs/base/common/strings';
import { IStorageService } from 'vs/platform/storage/common/storage';
interface IExtensionsSyncPreviewResult extends ISyncPreviewResult {
readonly localExtensions: ISyncExtension[];
@@ -35,14 +37,20 @@ interface ILastSyncUserData extends IRemoteUserData {
skippedExtensions: ISyncExtension[] | undefined;
}
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 2;
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/current.json` });
/*
Version 3 - Introduce installed property to skip installing built in extensions
*/
protected readonly version: number = 3;
protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); }
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@@ -53,14 +61,14 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(SyncResource.Extensions, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(SyncResource.Extensions, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
this._register(
Event.debounce(
Event.any<any>(
Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)),
this.extensionEnablementService.onDidChangeEnablement),
() => undefined, 500)(() => this._onDidChangeLocal.fire()));
() => undefined, 500)(() => this.triggerLocalChange()));
}
async pull(): Promise<void> {
@@ -79,9 +87,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
if (remoteUserData.syncData !== null) {
const localExtensions = await this.getLocalExtensions();
const remoteExtensions = this.parseExtensions(remoteUserData.syncData);
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], this.getIgnoredExtensions());
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
await this.apply({
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
@@ -112,8 +122,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
this.logService.info(`${this.syncResourceLogLabel}: Started pushing extensions...`);
this.setStatus(SyncStatus.Syncing);
const localExtensions = await this.getLocalExtensions();
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions());
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], ignoredExtensions);
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
await this.apply({
@@ -132,35 +144,58 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
async stop(): Promise<void> { }
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return [{ resource: joinPath(uri, 'extensions.json') }];
return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }];
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
return this.format(localExtensions);
}
let content = await super.resolveContent(uri);
if (content) {
return content;
}
content = await super.resolveContent(dirname(uri));
if (content) {
const syncData = this.parseSyncData(content);
if (syncData) {
switch (basename(uri)) {
case 'extensions.json':
const edits = format(syncData.content, undefined, {});
return applyEdits(syncData.content, edits);
return this.format(this.parseExtensions(syncData));
}
}
}
return null;
}
private format(extensions: ISyncExtension[]): string {
extensions.sort((e1, e2) => {
if (!e1.identifier.uuid && e2.identifier.uuid) {
return -1;
}
if (e1.identifier.uuid && !e2.identifier.uuid) {
return 1;
}
return compare(e1.identifier.id, e2.identifier.id);
});
const content = JSON.stringify(extensions);
const edits = format(content, undefined, {});
return applyEdits(content, edits);
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
async hasLocalData(): Promise<boolean> {
try {
const localExtensions = await this.getLocalExtensions();
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
if (isNonEmptyArray(localExtensions)) {
return true;
}
@@ -176,12 +211,28 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return SyncStatus.Idle;
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const syncExtensions = await this.parseAndMigrateExtensions(syncData);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
await this.apply({
added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: true
});
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreviewResult> {
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null;
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null;
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null;
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
const localExtensions = await this.getLocalExtensions();
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
if (remoteExtensions) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`);
@@ -189,7 +240,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
}
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions());
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
return {
added,
@@ -205,10 +256,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
};
}
private getIgnoredExtensions() {
return this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
}
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreviewResult, forcePush?: boolean): Promise<void> {
if (!hasLocalChanged && !hasRemoteChanged) {
@@ -216,9 +263,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
}
if (hasLocalChanged) {
// back up all disabled or market place extensions
const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid);
await this.backupLocal(JSON.stringify(backUpExtensions));
await this.backupLocal(JSON.stringify(localExtensions));
skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
}
@@ -317,31 +362,49 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return newSkippedExtensions;
}
private parseExtensions(syncData: ISyncData): ISyncExtension[] {
let extensions: ISyncExtension[] = JSON.parse(syncData.content);
if (syncData.version !== this.version) {
extensions = extensions.map(e => {
private async parseAndMigrateExtensions(syncData: ISyncData): Promise<ISyncExtension[]> {
const extensions = this.parseExtensions(syncData);
if (syncData.version === 1
|| syncData.version === 2
) {
const systemExtensions = await this.extensionManagementService.getInstalled(ExtensionType.System);
for (const extension of extensions) {
// #region Migration from v1 (enabled -> disabled)
if (!(<any>e).enabled) {
e.disabled = true;
if (syncData.version === 1) {
if ((<any>extension).enabled === false) {
extension.disabled = true;
}
delete (<any>extension).enabled;
}
delete (<any>e).enabled;
// #endregion
return e;
});
// #region Migration from v2 (set installed property on extension)
if (syncData.version === 2) {
if (systemExtensions.every(installed => !areSameExtensions(installed.identifier, extension.identifier))) {
extension.installed = true;
}
}
// #endregion
}
}
return extensions;
}
private async getLocalExtensions(): Promise<ISyncExtension[]> {
const installedExtensions = await this.extensionManagementService.getInstalled();
private parseExtensions(syncData: ISyncData): ISyncExtension[] {
return JSON.parse(syncData.content);
}
private getLocalExtensions(installedExtensions: ILocalExtension[]): ISyncExtension[] {
const disabledExtensions = this.extensionEnablementService.getDisabledExtensions();
return installedExtensions
.map(({ identifier }) => {
.map(({ identifier, type }) => {
const syncExntesion: ISyncExtension = { identifier };
if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) {
syncExntesion.disabled = true;
}
if (type === ExtensionType.User) {
syncExntesion.installed = true;
}
return syncExntesion;
});
}

View File

@@ -18,7 +18,7 @@ export interface IMergeResult {
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, previouslySkipped: string[], logService: ILogService): IMergeResult {
if (!remoteStorage) {
return { remote: localStorage, local: { added: {}, removed: [], updated: {} }, skipped: [] };
return { remote: Object.keys(localStorage).length > 0 ? localStorage : null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
}
const localToRemote = compare(localStorage, remoteStorage);
@@ -40,7 +40,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
skipped.push(key);
logService.info(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`);
logService.trace(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`);
continue;
}
if (storageKey.version !== remoteValue.version) {
@@ -64,7 +64,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
skipped.push(key);
logService.info(`GlobalState: Skipped updating ${key} in local storage as is not registered.`);
logService.trace(`GlobalState: Skipped updating ${key} in local storage as is not registered.`);
continue;
}
if (storageKey.version !== remoteValue.version) {
@@ -82,7 +82,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
for (const key of values(baseToRemote.removed)) {
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
logService.info(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
logService.trace(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
continue;
}
local.removed.push(key);
@@ -120,7 +120,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
// do not remove from remote if storage key is not found
if (!storageKey) {
skipped.push(key);
logService.info(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`);
logService.trace(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`);
continue;
}

View File

@@ -3,17 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { dirname, joinPath, basename } from 'vs/base/common/resources';
import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources';
import { IFileService } from 'vs/platform/files/common/files';
import { IStringDictionary } from 'vs/base/common/collections';
import { edit } from 'vs/platform/userDataSync/common/content';
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
import { parse } from 'vs/base/common/json';
import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { URI } from 'vs/base/common/uri';
@@ -41,6 +41,7 @@ interface ILastSyncUserData extends IRemoteUserData {
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/current.json` });
protected readonly version: number = 1;
constructor(
@@ -55,7 +56,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
@IStorageService private readonly storageService: IStorageService,
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(SyncResource.GlobalState, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
this._register(
Event.any(
@@ -65,7 +66,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key)),
/* Storage key registered */
this.storageKeysSyncRegistryService.onDidChangeStorageKeys
)((() => this._onDidChangeLocal.fire()))
)((() => this.triggerLocalChange()))
);
}
@@ -139,28 +140,44 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
async stop(): Promise<void> { }
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return [{ resource: joinPath(uri, 'globalState.json') }];
return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }];
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) {
const localGlobalState = await this.getLocalGlobalState();
return this.format(localGlobalState);
}
let content = await super.resolveContent(uri);
if (content) {
return content;
}
content = await super.resolveContent(dirname(uri));
if (content) {
const syncData = this.parseSyncData(content);
if (syncData) {
switch (basename(uri)) {
case 'globalState.json':
const edits = format(syncData.content, undefined, {});
return applyEdits(syncData.content, edits);
return this.format(JSON.parse(syncData.content));
}
}
}
return null;
}
private format(globalState: IGlobalState): string {
const storageKeys = Object.keys(globalState.storage).sort();
const storage: IStringDictionary<IStorageValue> = {};
storageKeys.forEach(key => storage[key] = globalState.storage[key]);
globalState.storage = storage;
const content = JSON.stringify(globalState);
const edits = format(content, undefined, {});
return applyEdits(content, edits);
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
@@ -183,6 +200,18 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
return SyncStatus.Idle;
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
const localUserData = await this.getLocalGlobalState();
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
const { local, skipped } = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
await this.apply({
local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData,
skippedStorageKeys: skipped,
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
hasRemoteChanged: true
});
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalSyncPreviewResult> {
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;

View File

@@ -16,10 +16,11 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { isUndefined } from 'vs/base/common/types';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
import { IStorageService } from 'vs/platform/storage/common/storage';
interface ISyncContent {
mac?: string;
@@ -42,10 +43,11 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
}
async pull(): Promise<void> {
@@ -212,6 +214,24 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
}
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const content = this.getKeybindingsContentFromSyncContent(syncData.content);
if (content !== null) {
const fileContent = await this.getLocalFileContent();
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasConflicts: false,
hasLocalChanged: true,
hasRemoteChanged: true,
}));
await this.apply();
}
}
private async apply(forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
@@ -248,10 +268,10 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== null || fileContent !== null)) {
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`);
const lastSyncContent = this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null);
await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: { version: remoteUserData.syncData!.version, content: lastSyncContent } });
const lastSyncContent = content !== null || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null;
await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, machineId: remoteUserData.syncData!.machineId, content: lastSyncContent } : null });
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`);
}
@@ -315,7 +335,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
getKeybindingsContentFromSyncContent(syncContent: string): string | null {
try {
const parsed = <ISyncContent>JSON.parse(syncContent);
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {

View File

@@ -429,26 +429,34 @@ function getEditToInsertAtLocation(content: string, key: string, value: any, loc
if (location.insertAfter) {
const edits: Edit[] = [];
/* Insert after a setting */
if (node.setting) {
return [{ offset: node.endOffset, length: 0, content: ',' + newProperty }];
edits.push({ offset: node.endOffset, length: 0, content: ',' + newProperty });
}
/*
Insert after a comment and before a setting (or)
Insert between comments and there is a setting after
*/
if (tree[location.index + 1] &&
(tree[location.index + 1].setting || findNextSettingNode(location.index, tree))) {
return [{ offset: node.endOffset, length: 0, content: eol + newProperty + ',' }];
/* Insert after a comment */
else {
const nextSettingNode = findNextSettingNode(location.index, tree);
const previousSettingNode = findPreviousSettingNode(location.index, tree);
const previousSettingCommaOffset = previousSettingNode?.setting?.commaOffset;
/* If there is a previous setting and it does not has comma then add it */
if (previousSettingNode && previousSettingCommaOffset === undefined) {
edits.push({ offset: previousSettingNode.endOffset, length: 0, content: ',' });
}
const isPreviouisSettingIncludesComment = previousSettingCommaOffset !== undefined && previousSettingCommaOffset > node.endOffset;
edits.push({
offset: isPreviouisSettingIncludesComment ? previousSettingCommaOffset! + 1 : node.endOffset,
length: 0,
content: nextSettingNode ? eol + newProperty + ',' : eol + newProperty
});
}
/* Insert after the comment at the end */
const edits = [{ offset: node.endOffset, length: 0, content: eol + newProperty }];
const previousSettingNode = findPreviousSettingNode(location.index, tree);
if (previousSettingNode && !previousSettingNode.setting!.hasCommaSeparator) {
edits.splice(0, 0, { offset: previousSettingNode.endOffset, length: 0, content: ',' });
}
return edits;
}
@@ -516,7 +524,7 @@ interface INode {
readonly value: string;
readonly setting?: {
readonly key: string;
readonly hasCommaSeparator: boolean;
readonly commaOffset: number | undefined;
};
readonly comment?: string;
}
@@ -547,7 +555,7 @@ function parseSettings(content: string): INode[] {
value: content.substring(startOffset, offset + length),
setting: {
key,
hasCommaSeparator: false
commaOffset: undefined
}
});
}
@@ -564,7 +572,7 @@ function parseSettings(content: string): INode[] {
value: content.substring(startOffset, offset + length),
setting: {
key,
hasCommaSeparator: false
commaOffset: undefined
}
});
}
@@ -577,7 +585,7 @@ function parseSettings(content: string): INode[] {
value: content.substring(startOffset, offset + length),
setting: {
key,
hasCommaSeparator: false
commaOffset: undefined
}
});
}
@@ -585,15 +593,21 @@ function parseSettings(content: string): INode[] {
onSeparator: (sep: string, offset: number, length: number) => {
if (hierarchyLevel === 0) {
if (sep === ',') {
const node = nodes.pop();
let index = nodes.length - 1;
for (; index >= 0; index--) {
if (nodes[index].setting) {
break;
}
}
const node = nodes[index];
if (node) {
nodes.push({
nodes.splice(index, 1, {
startOffset: node.startOffset,
endOffset: node.endOffset,
value: node.value,
setting: {
key: node.setting!.key,
hasCommaSeparator: true
commaOffset: offset
}
});
}

View File

@@ -14,11 +14,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { CancellationToken } from 'vs/base/common/cancellation';
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
import { edit } from 'vs/platform/userDataSync/common/content';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Edit } from 'vs/base/common/jsonFormatter';
import { setProperty, applyEdits } from 'vs/base/common/jsonEdit';
export interface ISettingsSyncContent {
settings: string;
@@ -41,6 +44,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
constructor(
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@@ -50,7 +54,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
@ITelemetryService telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
) {
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
}
protected setStatus(status: SyncStatus): void {
@@ -257,6 +261,27 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
}
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
if (settingsSyncContent) {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
const content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged: true,
hasRemoteChanged: true,
hasConflicts: false,
}));
await this.apply();
}
}
private async apply(forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
@@ -357,7 +382,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null;
}
private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null {
parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null {
try {
const parsed = <ISettingsSyncContent>JSON.parse(syncContent);
return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent };
@@ -391,4 +416,49 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
}
}
async recoverSettings(): Promise<void> {
try {
const fileContent = await this.getLocalFileContent();
if (!fileContent) {
return;
}
const syncData: ISyncData = JSON.parse(fileContent.value.toString());
if (!isSyncData(syncData)) {
return;
}
this.telemetryService.publicLog2('sync/settingsCorrupted');
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
if (!settingsSyncContent || !settingsSyncContent.settings) {
return;
}
let settings = settingsSyncContent.settings;
const formattingOptions = await this.getFormattingOptions();
for (const key in syncData) {
if (['version', 'content', 'machineId'].indexOf(key) === -1 && (syncData as any)[key] !== undefined) {
const edits: Edit[] = setProperty(settings, [key], (syncData as any)[key], formattingOptions);
if (edits.length) {
settings = applyEdits(settings, edits);
}
}
}
await this.fileService.writeFile(this.file, VSBuffer.fromString(settings));
} catch (e) {/* ignore */ }
}
}
function isSyncData(thing: any): thing is ISyncData {
if (thing
&& (thing.version !== undefined && typeof thing.version === 'number')
&& (thing.content !== undefined && typeof thing.content === 'string')
&& (thing.machineId !== undefined && typeof thing.machineId === 'string')
) {
return true;
}
return false;
}

View File

@@ -26,7 +26,7 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar
removed: values(removed),
updated,
conflicts: [],
remote: local
remote: Object.keys(local).length > 0 ? local : null
};
}

View File

@@ -16,6 +16,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
interface ISinppetsSyncPreviewResult extends ISyncPreviewResult {
readonly local: IStringDictionary<IFileContent>;
@@ -39,6 +40,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@@ -46,7 +48,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(SyncResource.Snippets, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(SyncResource.Snippets, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
this.snippetsFolder = environmentService.snippetsHome;
this.snippetsPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME);
this._register(this.fileService.watch(environmentService.userRoamingDataHome));
@@ -70,7 +72,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
// Otherwise fire change event
else {
this._onDidChangeLocal.fire();
this.triggerLocalChange();
}
}
@@ -256,6 +258,19 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const snippets = this.parseSnippets(syncData);
const { added, updated, removed } = merge(localSnippets, snippets, localSnippets);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: true
}));
await this.apply();
}
protected getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
@@ -285,7 +300,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
private async doGeneratePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null> = {}, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreviewResult> {
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null;
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
if (remoteSnippets) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);

View File

@@ -3,24 +3,29 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { timeout, Delayer } from 'vs/base/common/async';
import { Delayer, disposableTimeout } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService, ALL_SYNC_RESOURCES, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
type AutoSyncTriggerClassification = {
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
type AutoSyncClassification = {
sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
export const RESOURCE_ENABLEMENT_SOURCE = 'resourceEnablement';
export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService {
_serviceBrand: any;
private enabled: boolean = false;
private readonly autoSync = this._register(new MutableDisposable<AutoSync>());
private successiveFailures: number = 0;
private readonly syncDelayer: Delayer<void>;
private lastSyncTriggerTime: number | undefined = undefined;
private readonly syncTriggerDelayer: Delayer<void>;
private readonly _onError: Emitter<UserDataSyncError> = this._register(new Emitter<UserDataSyncError>());
readonly onError: Event<UserDataSyncError> = this._onError.event;
@@ -31,100 +36,157 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IProductService private readonly productService: IProductService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
this.updateEnablement(false, true);
this.syncDelayer = this._register(new Delayer<void>(0));
this._register(Event.any<any>(authTokenService.onDidChangeToken)(() => this.updateEnablement(true, true)));
this._register(Event.any<any>(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true)));
this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => this.updateEnablement(true, false)));
this._register(this.userDataSyncEnablementService.onDidChangeResourceEnablement(() => this.triggerAutoSync(['resourceEnablement'])));
this.syncTriggerDelayer = this._register(new Delayer<void>(0));
if (getUserDataSyncStore(this.productService, this.configurationService)) {
this.updateAutoSync();
this._register(Event.any(authTokenService.onDidChangeToken, this.userDataSyncEnablementService.onDidChangeEnablement)(() => this.updateAutoSync()));
this._register(Event.filter(this.userDataSyncEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerAutoSync([RESOURCE_ENABLEMENT_SOURCE])));
}
}
private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise<void> {
const { enabled, reason } = await this.isAutoSyncEnabled();
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
if (this.enabled) {
this.logService.info('Auto Sync: Started');
this.sync(true, auto);
return;
} else {
this.resetFailures();
if (stopIfDisabled) {
this.userDataSyncService.stop();
this.logService.info('Auto Sync: stopped because', reason);
}
}
}
private async sync(loop: boolean, auto: boolean): Promise<void> {
if (this.enabled) {
try {
await this.userDataSyncService.sync();
this.resetFailures();
} catch (e) {
const error = UserDataSyncError.toUserDataSyncError(e);
if (error.code === UserDataSyncErrorCode.TurnedOff || error.code === UserDataSyncErrorCode.SessionExpired) {
this.logService.info('Auto Sync: Sync is turned off in the cloud.');
this.logService.info('Auto Sync: Resetting the local sync state.');
await this.userDataSyncService.resetLocal();
this.logService.info('Auto Sync: Completed resetting the local sync state.');
if (auto) {
this.userDataSyncEnablementService.setEnablement(false);
this._onError.fire(error);
return;
} else {
return this.sync(loop, auto);
}
private updateAutoSync(): void {
const { enabled, reason } = this.isAutoSyncEnabled();
if (enabled) {
if (this.autoSync.value === undefined) {
this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncService, this.logService);
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()) {
this.autoSync.value.start();
}
this.logService.error(error);
this.successiveFailures++;
this._onError.fire(error);
}
if (loop) {
await timeout(1000 * 60 * 5);
this.sync(loop, true);
}
} else {
this.logService.trace('Auto Sync: Not syncing as it is disabled.');
if (this.autoSync.value !== undefined) {
this.logService.info('Auto Sync: Disabled because', reason);
this.autoSync.clear();
}
}
}
private async isAutoSyncEnabled(): Promise<{ enabled: boolean, reason?: string }> {
// For tests purpose only
protected startAutoSync(): boolean { return true; }
private isAutoSyncEnabled(): { enabled: boolean, reason?: string } {
if (!this.userDataSyncEnablementService.isEnabled()) {
return { enabled: false, reason: 'sync is disabled' };
}
if (this.userDataSyncService.status === SyncStatus.Uninitialized) {
return { enabled: false, reason: 'sync is not initialized' };
}
const token = await this.authTokenService.getToken();
if (!token) {
if (!this.authTokenService.token) {
return { enabled: false, reason: 'token is not avaialable' };
}
return { enabled: true };
}
private resetFailures(): void {
this.successiveFailures = 0;
private async onDidFinishSync(error: Error | undefined): Promise<void> {
if (!error) {
// Sync finished without errors
this.successiveFailures = 0;
return;
}
// Error while syncing
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff || userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
this.logService.info('Auto Sync: Sync is turned off in the cloud.');
await this.userDataSyncService.resetLocal();
this.logService.info('Auto Sync: Did reset the local sync state.');
this.userDataSyncEnablementService.setEnablement(false);
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
} else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests) {
this.userDataSyncEnablementService.setEnablement(false);
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
} else {
this.logService.error(userDataSyncError);
this.successiveFailures++;
}
this._onError.fire(userDataSyncError);
}
private sources: string[] = [];
async triggerAutoSync(sources: string[]): Promise<void> {
sources.forEach(source => this.telemetryService.publicLog2<{ source: string }, AutoSyncTriggerClassification>('sync/triggerAutoSync', { source }));
if (this.enabled) {
return this.syncDelayer.trigger(() => {
this.logService.info('Auto Sync: Triggered.');
return this.sync(false, true);
}, this.successiveFailures
? 1000 * 1 * Math.min(this.successiveFailures, 60) /* Delay by number of seconds as number of failures up to 1 minute */
: 1000);
} else {
this.syncDelayer.cancel();
if (this.autoSync.value === undefined) {
return this.syncTriggerDelayer.cancel();
}
/*
If sync is not triggered by sync resource (triggered by other sources like window focus etc.,) or by resource enablement
then limit sync to once per 10s
*/
const hasToLimitSync = sources.indexOf(RESOURCE_ENABLEMENT_SOURCE) === -1 && ALL_SYNC_RESOURCES.every(syncResource => sources.indexOf(syncResource) === -1);
if (hasToLimitSync && this.lastSyncTriggerTime
&& Math.round((new Date().getTime() - this.lastSyncTriggerTime) / 1000) < 10) {
this.logService.debug('Auto Sync Skipped: Limited to once per 10 seconds.');
return;
}
this.sources.push(...sources);
return this.syncTriggerDelayer.trigger(async () => {
this.telemetryService.publicLog2<{ sources: string[] }, AutoSyncClassification>('sync/triggered', { sources: this.sources });
this.sources = [];
if (this.autoSync.value) {
await this.autoSync.value.sync('Activity');
}
}, this.successiveFailures
? 1000 * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */
: 1000); /* Debounce for a second if there are no failures */
}
}
class AutoSync extends Disposable {
private static readonly INTERVAL_SYNCING = 'Interval';
private readonly intervalHandler = this._register(new MutableDisposable<IDisposable>());
private readonly _onDidStartSync = this._register(new Emitter<void>());
readonly onDidStartSync = this._onDidStartSync.event;
private readonly _onDidFinishSync = this._register(new Emitter<Error | undefined>());
readonly onDidFinishSync = this._onDidFinishSync.event;
constructor(
private readonly interval: number /* in milliseconds */,
private readonly userDataSyncService: IUserDataSyncService,
private readonly logService: IUserDataSyncLogService,
) {
super();
}
start(): void {
this._register(this.onDidFinishSync(() => this.waitUntilNextIntervalAndSync()));
this._register(toDisposable(() => {
this.userDataSyncService.stop();
this.logService.info('Auto Sync: Stopped');
}));
this.logService.info('Auto Sync: Started');
this.sync(AutoSync.INTERVAL_SYNCING);
}
private waitUntilNextIntervalAndSync(): void {
this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING), this.interval);
}
async sync(reason: string): Promise<void> {
this.logService.info(`Auto Sync: Triggered by ${reason}`);
this._onDidStartSync.fire();
let error: Error | undefined;
try {
await this.userDataSyncService.sync();
} catch (e) {
this.logService.error(e);
error = e;
}
this._onDidFinishSync.fire(error);
}
register<T extends IDisposable>(t: T): T {
return super._register(t);
}
}

View File

@@ -6,7 +6,6 @@
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 { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
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';
@@ -149,7 +148,7 @@ export const enum SyncResource {
export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState];
export interface IUserDataManifest {
latest?: Record<SyncResource, string>
latest?: Record<ServerResource, string>
session: string;
}
@@ -159,16 +158,17 @@ export interface IResourceRefHandle {
}
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
export type ServerResource = SyncResource | 'machines';
export interface IUserDataSyncStoreService {
_serviceBrand: undefined;
readonly userDataSyncStore: IUserDataSyncStore | undefined;
read(resource: SyncResource, oldValue: IUserData | null): Promise<IUserData>;
write(resource: SyncResource, content: string, ref: string | null): Promise<string>;
read(resource: ServerResource, oldValue: IUserData | null): Promise<IUserData>;
write(resource: ServerResource, content: string, ref: string | null): Promise<string>;
manifest(): Promise<IUserDataManifest | null>;
clear(): Promise<void>;
getAllRefs(resource: SyncResource): Promise<IResourceRefHandle[]>;
resolveContent(resource: SyncResource, ref: string): Promise<string | null>;
delete(resource: SyncResource): Promise<void>;
getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]>;
resolveContent(resource: ServerResource, ref: string): Promise<string | null>;
delete(resource: ServerResource): Promise<void>;
}
export const IUserDataSyncBackupStoreService = createDecorator<IUserDataSyncBackupStoreService>('IUserDataSyncBackupStoreService');
@@ -184,17 +184,20 @@ export interface IUserDataSyncBackupStoreService {
// #region User Data Sync Error
export enum UserDataSyncErrorCode {
// Server Errors
Unauthorized = 'Unauthorized',
Forbidden = 'Forbidden',
// 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',
RemotePreconditionFailed = 'RemotePreconditionFailed',
TooLarge = 'TooLarge',
NoRef = 'NoRef',
TurnedOff = 'TurnedOff',
SessionExpired = 'SessionExpired',
// Local Errors
LocalTooManyRequests = 'LocalTooManyRequests',
LocalPreconditionFailed = 'LocalPreconditionFailed',
LocalInvalidContent = 'LocalInvalidContent',
LocalError = 'LocalError',
@@ -223,7 +226,11 @@ export class UserDataSyncError extends Error {
}
export class UserDataSyncStoreError extends UserDataSyncError { }
export class UserDataSyncStoreError extends UserDataSyncError {
constructor(message: string, code: UserDataSyncErrorCode) {
super(message, code);
}
}
//#endregion
@@ -233,6 +240,7 @@ export interface ISyncExtension {
identifier: IExtensionIdentifier;
version?: string;
disabled?: boolean;
installed?: boolean;
}
export interface IStorageValue {
@@ -274,7 +282,8 @@ export interface IUserDataSynchroniser {
pull(): Promise<void>;
push(): Promise<void>;
sync(ref?: string): Promise<void>;
sync(manifest: IUserDataManifest | null): Promise<void>;
replace(uri: URI): Promise<boolean>;
stop(): Promise<void>;
getSyncPreview(): Promise<ISyncPreviewResult>
@@ -288,6 +297,7 @@ export interface IUserDataSynchroniser {
getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
getMachineId(syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
}
//#endregion
@@ -330,6 +340,7 @@ export interface IUserDataSyncService {
pull(): Promise<void>;
sync(): Promise<void>;
stop(): Promise<void>;
replace(uri: URI): Promise<void>;
reset(): Promise<void>;
resetLocal(): Promise<void>;
@@ -340,6 +351,7 @@ export interface IUserDataSyncService {
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
}
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
@@ -369,9 +381,6 @@ export interface IConflictSetting {
//#endregion
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false);
export const PREVIEW_DIR_NAME = 'preview';
export function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined {
if (localPreview.scheme === USER_DATA_SYNC_SCHEME) {

View File

@@ -11,10 +11,12 @@ import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
export class UserDataSyncChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncService) { }
constructor(private readonly service: IUserDataSyncService, private readonly logService: ILogService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
@@ -27,12 +29,23 @@ export class UserDataSyncChannel implements IServerChannel {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
async call(context: any, command: string, args?: any): Promise<any> {
try {
const result = await this._call(context, command, args);
return result;
} catch (e) {
this.logService.error(e);
throw e;
}
}
private _call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]);
case 'pull': return this.service.pull();
case 'sync': return this.service.sync();
case 'stop': this.service.stop(); return Promise.resolve();
case 'replace': return this.service.replace(URI.revive(args[0]));
case 'reset': return this.service.reset();
case 'resetLocal': return this.service.resetLocal();
case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge();
@@ -41,6 +54,7 @@ export class UserDataSyncChannel implements IServerChannel {
case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]);
case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(args[0]);
case 'getAssociatedResources': return this.service.getAssociatedResources(args[0], { created: args[1].created, uri: URI.revive(args[1].uri) });
case 'getMachineId': return this.service.getMachineId(args[0], { created: args[1].created, uri: URI.revive(args[1].uri) });
}
throw new Error('Invalid call');
}
@@ -151,3 +165,24 @@ export class StorageKeysSyncRegistryChannelClient extends Disposable implements
}
}
export class UserDataSyncMachinesServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncMachinesService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
async call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'getMachines': return this.service.getMachines();
case 'addCurrentMachine': return this.service.addCurrentMachine(args[0]);
case 'removeCurrentMachine': return this.service.removeCurrentMachine();
case 'renameMachine': return this.service.renameMachine(args[0], args[1]);
case 'disableMachine': return this.service.disableMachine(args[0]);
}
throw new Error('Invalid call');
}
}

View File

@@ -0,0 +1,158 @@
/*---------------------------------------------------------------------------------------------
* 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 { Disposable } from 'vs/base/common/lifecycle';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IUserDataSyncStoreService, IUserData, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { localize } from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
interface IMachineData {
id: string;
name: string;
disabled?: boolean;
}
interface IMachinesData {
version: number;
machines: IMachineData[];
}
export type IUserDataSyncMachine = Readonly<IMachineData> & { readonly isCurrent: boolean };
export const IUserDataSyncMachinesService = createDecorator<IUserDataSyncMachinesService>('IUserDataSyncMachinesService');
export interface IUserDataSyncMachinesService {
_serviceBrand: any;
getMachines(manifest?: IUserDataManifest): Promise<IUserDataSyncMachine[]>;
addCurrentMachine(name: string, manifest?: IUserDataManifest): Promise<void>;
removeCurrentMachine(manifest?: IUserDataManifest): Promise<void>;
renameMachine(machineId: string, name: string): Promise<void>;
disableMachine(machineId: string): Promise<void>
}
export class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService {
private static readonly VERSION = 1;
private static readonly RESOURCE = 'machines';
_serviceBrand: any;
private readonly currentMachineIdPromise: Promise<string>;
private userData: IUserData | null = null;
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IProductService private readonly productService: IProductService,
) {
super();
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
}
async getMachines(manifest?: IUserDataManifest): Promise<IUserDataSyncMachine[]> {
const currentMachineId = await this.currentMachineIdPromise;
const machineData = await this.readMachinesData(manifest);
return machineData.machines.map<IUserDataSyncMachine>(machine => ({ ...machine, ...{ isCurrent: machine.id === currentMachineId } }));
}
async addCurrentMachine(name: string, manifest?: IUserDataManifest): Promise<void> {
const currentMachineId = await this.currentMachineIdPromise;
const machineData = await this.readMachinesData(manifest);
let currentMachine = machineData.machines.find(({ id }) => id === currentMachineId);
if (currentMachine) {
currentMachine.name = name;
} else {
machineData.machines.push({ id: currentMachineId, name });
}
await this.writeMachinesData(machineData);
}
async removeCurrentMachine(manifest?: IUserDataManifest): Promise<void> {
const currentMachineId = await this.currentMachineIdPromise;
const machineData = await this.readMachinesData(manifest);
const updatedMachines = machineData.machines.filter(({ id }) => id !== currentMachineId);
if (updatedMachines.length !== machineData.machines.length) {
machineData.machines = updatedMachines;
await this.writeMachinesData(machineData);
}
}
async renameMachine(machineId: string, name: string, manifest?: IUserDataManifest): Promise<void> {
const machineData = await this.readMachinesData(manifest);
const currentMachine = machineData.machines.find(({ id }) => id === machineId);
if (currentMachine) {
currentMachine.name = name;
await this.writeMachinesData(machineData);
}
}
async disableMachine(machineId: string): Promise<void> {
const machineData = await this.readMachinesData();
const machine = machineData.machines.find(({ id }) => id === machineId);
if (machine) {
machine.disabled = true;
await this.writeMachinesData(machineData);
}
}
private async readMachinesData(manifest?: IUserDataManifest): Promise<IMachinesData> {
this.userData = await this.readUserData(manifest);
const machinesData = this.parse(this.userData);
if (machinesData.version !== UserDataSyncMachinesService.VERSION) {
throw new Error(localize('error incompatible', "Cannot read machines data as the current version is incompatible. Please update {0} and try again.", this.productService.nameLong));
}
return machinesData;
}
private async writeMachinesData(machinesData: IMachinesData): Promise<void> {
const content = JSON.stringify(machinesData);
const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null);
this.userData = { ref, content };
}
private async readUserData(manifest?: IUserDataManifest): Promise<IUserData> {
if (this.userData) {
const latestRef = manifest && manifest.latest ? manifest.latest[UserDataSyncMachinesService.RESOURCE] : undefined;
// Last time synced resource and latest resource on server are same
if (this.userData.ref === latestRef) {
return this.userData;
}
// There is no resource on server and last time it was synced with no resource
if (latestRef === undefined && this.userData.content === null) {
return this.userData;
}
}
return this.userDataSyncStoreService.read(UserDataSyncMachinesService.RESOURCE, this.userData);
}
private parse(userData: IUserData): IMachinesData {
if (userData.content !== null) {
try {
return JSON.parse(userData.content);
} catch (e) {
this.logService.error(e);
}
}
return {
version: UserDataSyncMachinesService.VERSION,
machines: []
};
}
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
@@ -19,9 +19,14 @@ import { URI } from 'vs/base/common/uri';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { isEqual } from 'vs/base/common/resources';
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
import { Throttler } from 'vs/base/common/async';
import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { IProductService } from 'vs/platform/product/common/productService';
import { platform, PlatformToString } from 'vs/base/common/platform';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
type SyncErrorClassification = {
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
type SyncClassification = {
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
const SESSION_ID_KEY = 'sync.sessionId';
@@ -31,6 +36,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
_serviceBrand: any;
private readonly syncThrottler: Throttler;
private readonly synchronisers: IUserDataSynchroniser[];
private _status: SyncStatus = SyncStatus.Uninitialized;
@@ -65,9 +71,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
@IProductService private readonly productService: IProductService
) {
super();
this.syncThrottler = new Throttler();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
this.snippetsSynchroniser = this._register(this.instantiationService.createInstance(SnippetsSynchroniser));
@@ -87,31 +96,55 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
async pull(): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.pull();
} catch (e) {
this.handleSyncError(e, synchroniser.resource);
try {
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.pull();
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
}
}
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
}
throw error;
}
this.updateLastSyncTime();
}
async push(): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.push();
} catch (e) {
this.handleSyncError(e, synchroniser.resource);
try {
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.push();
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
}
}
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
}
throw error;
}
this.updateLastSyncTime();
}
private recoveredSettings: boolean = false;
async sync(): Promise<void> {
await this.checkEnablement();
if (!this.recoveredSettings) {
await this.settingsSynchroniser.recoverSettings();
this.recoveredSettings = true;
}
await this.syncThrottler.queue(() => this.doSync());
}
private async doSync(): Promise<void> {
const startTime = new Date().getTime();
this._syncErrors = [];
try {
@@ -120,11 +153,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.setStatus(SyncStatus.Syncing);
}
this.telemetryService.publicLog2('sync/getmanifest');
let manifest = await this.userDataSyncStoreService.manifest();
// Server has no data but this machine was synced before
if (manifest === null && await this.hasPreviouslySynced()) {
// Sync was turned off from other machine
// Sync was turned off in the cloud
throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
}
@@ -134,11 +168,22 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
throw new UserDataSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
}
const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined);
const currentMachine = machines.find(machine => machine.isCurrent);
// Check if sync was turned off from other machine
if (currentMachine?.disabled) {
// Unset the current machine
await this.userDataSyncMachinesService.removeCurrentMachine(manifest || undefined);
// Throw TurnedOff error
throw new UserDataSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
}
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resource] : undefined);
await synchroniser.sync(manifest);
} catch (e) {
this.handleSyncError(e, synchroniser.resource);
this.handleSynchronizerError(e, synchroniser.resource);
this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]);
}
}
@@ -153,15 +198,34 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL);
}
if (!currentMachine) {
const name = this.computeDefaultMachineName(machines);
await this.userDataSyncMachinesService.addCurrentMachine(name, manifest || undefined);
}
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
}
throw error;
} finally {
this.updateStatus();
this._onSyncErrors.fire(this._syncErrors);
}
}
async replace(uri: URI): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
if (await synchroniser.replace(uri)) {
return;
}
}
}
async stop(): Promise<void> {
await this.checkEnablement();
if (this.status === SyncStatus.Idle) {
@@ -209,6 +273,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle);
}
getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined> {
return this.getSynchroniser(resource).getMachineId(syncResourceHandle);
}
async isFirstTimeSyncWithMerge(): Promise<boolean> {
await this.checkEnablement();
if (!await this.userDataSyncStoreService.manifest()) {
@@ -232,16 +300,19 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
async reset(): Promise<void> {
await this.checkEnablement();
await this.resetRemote();
await this.resetLocal();
await this.resetLocal(true);
}
async resetLocal(): Promise<void> {
async resetLocal(donotUnsetMachine?: boolean): Promise<void> {
await this.checkEnablement();
this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL);
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL);
if (!donotUnsetMachine) {
await this.userDataSyncMachinesService.removeCurrentMachine();
}
for (const synchroniser of this.synchronisers) {
try {
synchroniser.resetLocal();
await synchroniser.resetLocal();
} catch (e) {
this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
this.logService.error(e);
@@ -322,13 +393,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
private handleSyncError(e: Error, source: SyncResource): void {
if (e instanceof UserDataSyncStoreError) {
private handleSynchronizerError(e: Error, source: SyncResource): void {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.TooLarge:
this.telemetryService.publicLog2<{ source: string }, SyncErrorClassification>('sync/errorTooLarge', { source });
case UserDataSyncErrorCode.TooManyRequests:
case UserDataSyncErrorCode.LocalTooManyRequests:
case UserDataSyncErrorCode.UpgradeRequired:
case UserDataSyncErrorCode.Incompatible:
throw e;
}
throw e;
}
this.logService.error(e);
this.logService.error(`${source}: ${toErrorMessage(e)}`);
@@ -339,6 +413,20 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts }));
}
private computeDefaultMachineName(machines: IUserDataSyncMachine[]): string {
const namePrefix = `${this.productService.nameLong} (${PlatformToString(platform)})`;
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`);
let nameIndex = 0;
for (const machine of machines) {
const matches = nameRegEx.exec(machine.name);
const index = matches ? parseInt(matches[1]) : 0;
nameIndex = index > nameIndex ? index : nameIndex;
}
return `${namePrefix} #${nameIndex + 1}`;
}
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
return this.synchronisers.filter(s => s.resource === source)[0];
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, } from 'vs/base/common/lifecycle';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, SyncResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } 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';
@@ -15,9 +15,15 @@ import { IProductService } 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';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { assign } from 'vs/base/common/objects';
import { generateUuid } from 'vs/base/common/uuid';
import { isWeb } from 'vs/base/common/platform';
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 */
export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService {
@@ -25,6 +31,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
readonly userDataSyncStore: IUserDataSyncStore | undefined;
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
private readonly session: RequestsSession;
constructor(
@IProductService productService: IProductService,
@@ -34,21 +41,28 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.userDataSyncStore = getUserDataSyncStore(productService, configurationService);
this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService)
.then(uuid => {
const headers: IHeaders = {
'X-Sync-Client-Id': productService.version,
'X-Client-Name': `${productService.applicationName}${isWeb ? '-web' : ''}`,
'X-Client-Version': productService.version,
'X-Machine-Id': uuid
};
headers['X-Sync-Machine-Id'] = uuid;
if (productService.commit) {
headers['X-Client-Commit'] = productService.commit;
}
return headers;
});
/* A requests session that limits requests per sessions */
this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService);
}
async getAllRefs(resource: SyncResource): Promise<IResourceRefHandle[]> {
async getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -56,17 +70,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const uri = joinPath(this.userDataSyncStore.url, 'resource', resource);
const headers: IHeaders = {};
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
const result = await asJson<{ url: string, created: number }[]>(context) || [];
return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ }));
}
async resolveContent(resource: SyncResource, ref: string): Promise<string | null> {
async resolveContent(resource: ServerResource, ref: string): Promise<string | null> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -75,17 +89,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const headers: IHeaders = {};
headers['Cache-Control'] = 'no-cache';
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
const content = await asText(context);
return content;
}
async delete(resource: SyncResource): Promise<void> {
async delete(resource: ServerResource): Promise<void> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -93,14 +107,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
const headers: IHeaders = {};
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
}
async read(resource: SyncResource, oldValue: IUserData | null): Promise<IUserData> {
async read(resource: ServerResource, oldValue: IUserData | null): Promise<IUserData> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -113,7 +127,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
headers['If-None-Match'] = oldValue.ref;
}
const context = await this.request({ type: 'GET', url, headers }, resource, CancellationToken.None);
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (context.res.statusCode === 304) {
// There is no new value. Hence return the old value.
@@ -121,18 +135,18 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
const ref = context.res.headers['etag'];
if (!ref) {
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource);
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef);
}
const content = await asText(context);
return { ref, content };
}
async write(resource: SyncResource, data: string, ref: string | null): Promise<string> {
async write(resource: ServerResource, data: string, ref: string | null): Promise<string> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -143,15 +157,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
headers['If-Match'] = ref;
}
const context = await this.request({ type: 'POST', url, data, headers }, resource, CancellationToken.None);
const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
const newRef = context.res.headers['etag'];
if (!newRef) {
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource);
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef);
}
return newRef;
}
@@ -164,12 +178,30 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const url = joinPath(this.userDataSyncStore.url, 'manifest').toString();
const headers: IHeaders = { 'Content-Type': 'application/json' };
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
return asJson(context);
const manifest = await asJson<IUserDataManifest>(context);
const currentSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
if (currentSessionId && manifest && currentSessionId !== manifest.session) {
// Server session is different from client session so clear cached session.
this.clearSession();
}
if (manifest === null && currentSessionId) {
// server session is cleared so clear cached session.
this.clearSession();
}
if (manifest) {
// update session
this.storageService.store(USER_SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL);
}
return manifest;
}
async clear(): Promise<void> {
@@ -180,17 +212,25 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const url = joinPath(this.userDataSyncStore.url, 'resource').toString();
const headers: IHeaders = { 'Content-Type': 'text/plain' };
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
// clear cached session.
this.clearSession();
}
private async request(options: IRequestOptions, source: SyncResource | undefined, token: CancellationToken): Promise<IRequestContext> {
const authToken = await this.authTokenService.getToken();
private clearSession(): void {
this.storageService.remove(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL);
}
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
const authToken = this.authTokenService.token;
if (!authToken) {
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, source);
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized);
}
const commonHeaders = await this.commonHeadersPromise;
@@ -199,34 +239,95 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
'authorization': `Bearer ${authToken.token}`,
});
// Add session headers
this.addSessionHeaders(options.headers);
this.logService.trace('Sending request to server', { url: options.url, type: options.type, headers: { ...options.headers, ...{ authorization: undefined } } });
let context;
try {
context = await this.requestService.request(options, token);
context = await this.session.request(options, token);
this.logService.trace('Request finished', { url: options.url, status: context.res.statusCode });
} catch (e) {
throw new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, source);
if (!(e instanceof UserDataSyncStoreError)) {
e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused);
}
throw e;
}
if (context.res.statusCode === 401) {
this.authTokenService.sendTokenFailed();
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source);
}
if (context.res.statusCode === 403) {
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' is Forbidden (403).`, UserDataSyncErrorCode.Forbidden, source);
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized);
}
if (context.res.statusCode === 412) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.RemotePreconditionFailed, source);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed);
}
if (context.res.statusCode === 413) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge, source);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge);
}
if (context.res.statusCode === 426) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed with status Upgrade Required (426). Please upgrade the client and try again.`, UserDataSyncErrorCode.UpgradeRequired);
}
if (context.res.statusCode === 429) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests);
}
return context;
}
private addSessionHeaders(headers: IHeaders): void {
let machineSessionId = this.storageService.get(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL);
if (machineSessionId === undefined) {
machineSessionId = generateUuid();
this.storageService.store(MACHINE_SESSION_ID_KEY, machineSessionId, StorageScope.GLOBAL);
}
headers['X-Machine-Session-Id'] = machineSessionId;
const userSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
if (userSessionId !== undefined) {
headers['X-User-Session-Id'] = userSessionId;
}
}
}
export class RequestsSession {
private count: number = 0;
private startTime: Date | undefined = undefined;
constructor(
private readonly limit: number,
private readonly interval: number, /* in ms */
private readonly requestService: IRequestService,
) { }
request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
if (this.isExpired()) {
this.reset();
}
if (this.count >= this.limit) {
throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests);
}
this.startTime = this.startTime || new Date();
this.count++;
return this.requestService.request(options, token);
}
private isExpired(): boolean {
return this.startTime !== undefined && new Date().getTime() - this.startTime.getTime() > this.interval;
}
private reset(): void {
this.count = 0;
this.startTime = undefined;
}
}