mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-06 17:23:53 -05:00
Merge from vscode 79a1f5a5ca0c6c53db617aa1fa5a2396d2caebe2
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user