"Fossies" - the Fresh Open Source Software Archive

Member "vscode-1.49.1/src/vs/platform/userDataSync/common/abstractSynchronizer.ts" (16 Sep 2020, 36744 Bytes) of package /linux/misc/vscode-1.49.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "abstractSynchronizer.ts": 1.48.2_vs_1.49.0.

    1 /*---------------------------------------------------------------------------------------------
    2  *  Copyright (c) Microsoft Corporation. All rights reserved.
    3  *  Licensed under the MIT License. See License.txt in the project root for license information.
    4  *--------------------------------------------------------------------------------------------*/
    5 
    6 import { Disposable } from 'vs/base/common/lifecycle';
    7 import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
    8 import { VSBuffer } from 'vs/base/common/buffer';
    9 import { URI } from 'vs/base/common/uri';
   10 import {
   11     SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
   12     IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview,
   13     IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change, MergeState
   14 } from 'vs/platform/userDataSync/common/userDataSync';
   15 import { IEnvironmentService } from 'vs/platform/environment/common/environment';
   16 import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
   17 import { CancelablePromise, RunOnceScheduler, createCancelablePromise } from 'vs/base/common/async';
   18 import { Emitter, Event } from 'vs/base/common/event';
   19 import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
   20 import { ParseError, parse } from 'vs/base/common/json';
   21 import { FormattingOptions } from 'vs/base/common/jsonFormatter';
   22 import { IStringDictionary } from 'vs/base/common/collections';
   23 import { localize } from 'vs/nls';
   24 import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
   25 import { isString } from 'vs/base/common/types';
   26 import { uppercaseFirstLetter } from 'vs/base/common/strings';
   27 import { equals } from 'vs/base/common/arrays';
   28 import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
   29 import { IStorageService } from 'vs/platform/storage/common/storage';
   30 import { CancellationToken } from 'vs/base/common/cancellation';
   31 import { IHeaders } from 'vs/base/parts/request/common/request';
   32 
   33 type SyncSourceClassification = {
   34     source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
   35 };
   36 
   37 function isSyncData(thing: any): thing is ISyncData {
   38     if (thing
   39         && (thing.version !== undefined && typeof thing.version === 'number')
   40         && (thing.content !== undefined && typeof thing.content === 'string')) {
   41 
   42         // backward compatibility
   43         if (Object.keys(thing).length === 2) {
   44             return true;
   45         }
   46 
   47         if (Object.keys(thing).length === 3
   48             && (thing.machineId !== undefined && typeof thing.machineId === 'string')) {
   49             return true;
   50         }
   51     }
   52 
   53     return false;
   54 }
   55 
   56 function getLastSyncResourceUri(syncResource: SyncResource, environmentService: IEnvironmentService): URI {
   57     return joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`);
   58 }
   59 
   60 export interface IResourcePreview {
   61 
   62     readonly remoteResource: URI;
   63     readonly remoteContent: string | null;
   64     readonly remoteChange: Change;
   65 
   66     readonly localResource: URI;
   67     readonly localContent: string | null;
   68     readonly localChange: Change;
   69 
   70     readonly previewResource: URI;
   71     readonly acceptedResource: URI;
   72 }
   73 
   74 export interface IAcceptResult {
   75     readonly content: string | null;
   76     readonly localChange: Change;
   77     readonly remoteChange: Change;
   78 }
   79 
   80 export interface IMergeResult extends IAcceptResult {
   81     readonly hasConflicts: boolean;
   82 }
   83 
   84 interface IEditableResourcePreview extends IBaseResourcePreview, IResourcePreview {
   85     localChange: Change;
   86     remoteChange: Change;
   87     mergeState: MergeState;
   88     acceptResult?: IAcceptResult;
   89 }
   90 
   91 interface ISyncResourcePreview extends IBaseSyncResourcePreview {
   92     readonly remoteUserData: IRemoteUserData;
   93     readonly lastSyncUserData: IRemoteUserData | null;
   94     readonly resourcePreviews: IEditableResourcePreview[];
   95 }
   96 
   97 export abstract class AbstractSynchroniser extends Disposable {
   98 
   99     private syncPreviewPromise: CancelablePromise<ISyncResourcePreview> | null = null;
  100 
  101     protected readonly syncFolder: URI;
  102     protected readonly syncPreviewFolder: URI;
  103     private readonly currentMachineIdPromise: Promise<string>;
  104 
  105     private _status: SyncStatus = SyncStatus.Idle;
  106     get status(): SyncStatus { return this._status; }
  107     private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
  108     readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
  109 
  110     private _conflicts: IBaseResourcePreview[] = [];
  111     get conflicts(): IBaseResourcePreview[] { return this._conflicts; }
  112     private _onDidChangeConflicts: Emitter<IBaseResourcePreview[]> = this._register(new Emitter<IBaseResourcePreview[]>());
  113     readonly onDidChangeConflicts: Event<IBaseResourcePreview[]> = this._onDidChangeConflicts.event;
  114 
  115     private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
  116     private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
  117     readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
  118 
  119     protected readonly lastSyncResource: URI;
  120     protected readonly syncResourceLogLabel: string;
  121 
  122     private syncHeaders: IHeaders = {};
  123 
  124     constructor(
  125         readonly resource: SyncResource,
  126         @IFileService protected readonly fileService: IFileService,
  127         @IEnvironmentService protected readonly environmentService: IEnvironmentService,
  128         @IStorageService storageService: IStorageService,
  129         @IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
  130         @IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
  131         @IUserDataSyncResourceEnablementService protected readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
  132         @ITelemetryService protected readonly telemetryService: ITelemetryService,
  133         @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
  134         @IConfigurationService protected readonly configurationService: IConfigurationService,
  135     ) {
  136         super();
  137         this.syncResourceLogLabel = uppercaseFirstLetter(this.resource);
  138         this.syncFolder = joinPath(environmentService.userDataSyncHome, resource);
  139         this.syncPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME);
  140         this.lastSyncResource = getLastSyncResourceUri(resource, environmentService);
  141         this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
  142     }
  143 
  144     protected isEnabled(): boolean { return this.userDataSyncResourceEnablementService.isResourceEnabled(this.resource); }
  145 
  146     protected async triggerLocalChange(): Promise<void> {
  147         if (this.isEnabled()) {
  148             this.localChangeTriggerScheduler.schedule();
  149         }
  150     }
  151 
  152     protected async doTriggerLocalChange(): Promise<void> {
  153 
  154         // Sync again if current status is in conflicts
  155         if (this.status === SyncStatus.HasConflicts) {
  156             this.logService.info(`${this.syncResourceLogLabel}: In conflicts state and local change detected. Syncing again...`);
  157             const preview = await this.syncPreviewPromise!;
  158             this.syncPreviewPromise = null;
  159             const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, true);
  160             this.setStatus(status);
  161         }
  162 
  163         // Check if local change causes remote change
  164         else {
  165             this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
  166             const lastSyncUserData = await this.getLastSyncUserData();
  167             const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, true, CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true;
  168             if (hasRemoteChanged) {
  169                 this._onDidChangeLocal.fire();
  170             }
  171         }
  172     }
  173 
  174     protected setStatus(status: SyncStatus): void {
  175         if (this._status !== status) {
  176             const oldStatus = this._status;
  177             if (status === SyncStatus.HasConflicts) {
  178                 // Log to telemetry when there is a sync conflict
  179                 this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsDetected', { source: this.resource });
  180             }
  181             if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) {
  182                 // Log to telemetry when conflicts are resolved
  183                 this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.resource });
  184             }
  185             this._status = status;
  186             this._onDidChangStatus.fire(status);
  187         }
  188     }
  189 
  190     async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<void> {
  191         await this._sync(manifest, true, headers);
  192     }
  193 
  194     async preview(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
  195         return this._sync(manifest, false, headers);
  196     }
  197 
  198     async apply(force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
  199         try {
  200             this.syncHeaders = { ...headers };
  201 
  202             const status = await this.doApply(force);
  203             this.setStatus(status);
  204 
  205             return this.syncPreviewPromise;
  206         } finally {
  207             this.syncHeaders = {};
  208         }
  209     }
  210 
  211     private async _sync(manifest: IUserDataManifest | null, apply: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null> {
  212         try {
  213             this.syncHeaders = { ...headers };
  214 
  215             if (!this.isEnabled()) {
  216                 if (this.status !== SyncStatus.Idle) {
  217                     await this.stop();
  218                 }
  219                 this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
  220                 return null;
  221             }
  222 
  223             if (this.status === SyncStatus.HasConflicts) {
  224                 this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`);
  225                 return this.syncPreviewPromise;
  226             }
  227 
  228             if (this.status === SyncStatus.Syncing) {
  229                 this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`);
  230                 return this.syncPreviewPromise;
  231             }
  232 
  233             this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
  234             this.setStatus(SyncStatus.Syncing);
  235 
  236             let status: SyncStatus = SyncStatus.Idle;
  237             try {
  238                 const lastSyncUserData = await this.getLastSyncUserData();
  239                 const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
  240                 status = await this.performSync(remoteUserData, lastSyncUserData, apply);
  241                 if (status === SyncStatus.HasConflicts) {
  242                     this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`);
  243                 } else if (status === SyncStatus.Idle) {
  244                     this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`);
  245                 }
  246                 return this.syncPreviewPromise || null;
  247             } finally {
  248                 this.setStatus(status);
  249             }
  250         } finally {
  251             this.syncHeaders = {};
  252         }
  253     }
  254 
  255     async replace(uri: URI): Promise<boolean> {
  256         const content = await this.resolveContent(uri);
  257         if (!content) {
  258             return false;
  259         }
  260 
  261         const syncData = this.parseSyncData(content);
  262         if (!syncData) {
  263             return false;
  264         }
  265 
  266         await this.stop();
  267 
  268         try {
  269             this.logService.trace(`${this.syncResourceLogLabel}: Started resetting ${this.resource.toLowerCase()}...`);
  270             this.setStatus(SyncStatus.Syncing);
  271             const lastSyncUserData = await this.getLastSyncUserData();
  272             const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
  273 
  274             /* use replace sync data */
  275             const resourcePreviewResults = await this.generateSyncPreview({ ref: remoteUserData.ref, syncData }, lastSyncUserData, CancellationToken.None);
  276 
  277             const resourcePreviews: [IResourcePreview, IAcceptResult][] = [];
  278             for (const resourcePreviewResult of resourcePreviewResults) {
  279                 /* Accept remote resource */
  280                 const acceptResult: IAcceptResult = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.remoteResource, undefined, CancellationToken.None);
  281                 /* compute remote change */
  282                 const { remoteChange } = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, resourcePreviewResult.remoteContent, CancellationToken.None);
  283                 resourcePreviews.push([resourcePreviewResult, { ...acceptResult, remoteChange: remoteChange !== Change.None ? remoteChange : Change.Modified }]);
  284             }
  285 
  286             await this.applyResult(remoteUserData, lastSyncUserData, resourcePreviews, false);
  287             this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
  288         } finally {
  289             this.setStatus(SyncStatus.Idle);
  290         }
  291 
  292         return true;
  293     }
  294 
  295     protected async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
  296         if (lastSyncUserData) {
  297 
  298             const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined;
  299 
  300             // Last time synced resource and latest resource on server are same
  301             if (lastSyncUserData.ref === latestRef) {
  302                 return lastSyncUserData;
  303             }
  304 
  305             // There is no resource on server and last time it was synced with no resource
  306             if (latestRef === undefined && lastSyncUserData.syncData === null) {
  307                 return lastSyncUserData;
  308             }
  309         }
  310         return this.getRemoteUserData(lastSyncUserData);
  311     }
  312 
  313     private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
  314         if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
  315             // current version is not compatible with cloud version
  316             this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource });
  317             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.IncompatibleLocalContent, this.resource);
  318         }
  319 
  320         try {
  321             return await this.doSync(remoteUserData, lastSyncUserData, apply);
  322         } catch (e) {
  323             if (e instanceof UserDataSyncError) {
  324                 switch (e.code) {
  325 
  326                     case UserDataSyncErrorCode.LocalPreconditionFailed:
  327                         // Rejected as there is a new local version. Syncing again...
  328                         this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`);
  329                         return this.performSync(remoteUserData, lastSyncUserData, apply);
  330 
  331                     case UserDataSyncErrorCode.Conflict:
  332                     case UserDataSyncErrorCode.PreconditionFailed:
  333                         // Rejected as there is a new remote version. Syncing again...
  334                         this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`);
  335 
  336                         // Avoid cache and get latest remote user data - https://github.com/microsoft/vscode/issues/90624
  337                         remoteUserData = await this.getRemoteUserData(null);
  338 
  339                         // Get the latest last sync user data. Because multiples parallel syncs (in Web) could share same last sync data
  340                         // and one of them successfully updated remote and last sync state.
  341                         lastSyncUserData = await this.getLastSyncUserData();
  342 
  343                         return this.performSync(remoteUserData, lastSyncUserData, apply);
  344                 }
  345             }
  346             throw e;
  347         }
  348     }
  349 
  350     protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
  351         try {
  352             // generate or use existing preview
  353             if (!this.syncPreviewPromise) {
  354                 this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, apply, token));
  355             }
  356 
  357             const preview = await this.syncPreviewPromise;
  358             this.updateConflicts(preview.resourcePreviews);
  359             if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
  360                 return SyncStatus.HasConflicts;
  361             }
  362 
  363             if (apply) {
  364                 return await this.doApply(false);
  365             }
  366 
  367             return SyncStatus.Syncing;
  368 
  369         } catch (error) {
  370 
  371             // reset preview on error
  372             this.syncPreviewPromise = null;
  373 
  374             throw error;
  375         }
  376     }
  377 
  378     async merge(resource: URI): Promise<ISyncResourcePreview | null> {
  379         await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
  380             const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
  381             await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult?.content || ''));
  382             const acceptResult: IAcceptResult | undefined = mergeResult && !mergeResult.hasConflicts
  383                 ? await this.getAcceptResult(resourcePreview, resourcePreview.previewResource, undefined, CancellationToken.None)
  384                 : undefined;
  385             resourcePreview.acceptResult = acceptResult;
  386             resourcePreview.mergeState = mergeResult.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview;
  387             resourcePreview.localChange = acceptResult ? acceptResult.localChange : mergeResult.localChange;
  388             resourcePreview.remoteChange = acceptResult ? acceptResult.remoteChange : mergeResult.remoteChange;
  389             return resourcePreview;
  390         });
  391         return this.syncPreviewPromise;
  392     }
  393 
  394     async accept(resource: URI, content?: string | null): Promise<ISyncResourcePreview | null> {
  395         await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
  396             const acceptResult = await this.getAcceptResult(resourcePreview, resource, content, CancellationToken.None);
  397             resourcePreview.acceptResult = acceptResult;
  398             resourcePreview.mergeState = MergeState.Accepted;
  399             resourcePreview.localChange = acceptResult.localChange;
  400             resourcePreview.remoteChange = acceptResult.remoteChange;
  401             return resourcePreview;
  402         });
  403         return this.syncPreviewPromise;
  404     }
  405 
  406     async discard(resource: URI): Promise<ISyncResourcePreview | null> {
  407         await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
  408             const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
  409             await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult.content || ''));
  410             resourcePreview.acceptResult = undefined;
  411             resourcePreview.mergeState = MergeState.Preview;
  412             resourcePreview.localChange = mergeResult.localChange;
  413             resourcePreview.remoteChange = mergeResult.remoteChange;
  414             return resourcePreview;
  415         });
  416         return this.syncPreviewPromise;
  417     }
  418 
  419     private async updateSyncResourcePreview(resource: URI, updateResourcePreview: (resourcePreview: IEditableResourcePreview) => Promise<IEditableResourcePreview>): Promise<void> {
  420         if (!this.syncPreviewPromise) {
  421             return;
  422         }
  423 
  424         let preview = await this.syncPreviewPromise;
  425         const index = preview.resourcePreviews.findIndex(({ localResource, remoteResource, previewResource }) =>
  426             isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
  427         if (index === -1) {
  428             return;
  429         }
  430 
  431         this.syncPreviewPromise = createCancelablePromise(async token => {
  432             const resourcePreviews = [...preview.resourcePreviews];
  433             resourcePreviews[index] = await updateResourcePreview(resourcePreviews[index]);
  434             return {
  435                 ...preview,
  436                 resourcePreviews
  437             };
  438         });
  439 
  440         preview = await this.syncPreviewPromise;
  441         this.updateConflicts(preview.resourcePreviews);
  442         if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
  443             this.setStatus(SyncStatus.HasConflicts);
  444         } else {
  445             this.setStatus(SyncStatus.Syncing);
  446         }
  447     }
  448 
  449     private async doApply(force: boolean): Promise<SyncStatus> {
  450         if (!this.syncPreviewPromise) {
  451             return SyncStatus.Idle;
  452         }
  453 
  454         const preview = await this.syncPreviewPromise;
  455 
  456         // check for conflicts
  457         if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
  458             return SyncStatus.HasConflicts;
  459         }
  460 
  461         // check if all are accepted
  462         if (preview.resourcePreviews.some(({ mergeState }) => mergeState !== MergeState.Accepted)) {
  463             return SyncStatus.Syncing;
  464         }
  465 
  466         // apply preview
  467         await this.applyResult(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews.map(resourcePreview => ([resourcePreview, resourcePreview.acceptResult!])), force);
  468 
  469         // reset preview
  470         this.syncPreviewPromise = null;
  471 
  472         // reset preview folder
  473         await this.clearPreviewFolder();
  474 
  475         return SyncStatus.Idle;
  476     }
  477 
  478     private async clearPreviewFolder(): Promise<void> {
  479         try {
  480             await this.fileService.del(this.syncPreviewFolder, { recursive: true });
  481         } catch (error) { /* Ignore */ }
  482     }
  483 
  484     private updateConflicts(resourcePreviews: IEditableResourcePreview[]): void {
  485         const conflicts = resourcePreviews.filter(({ mergeState }) => mergeState === MergeState.Conflict);
  486         if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) {
  487             this._conflicts = conflicts;
  488             this._onDidChangeConflicts.fire(conflicts);
  489         }
  490     }
  491 
  492     async hasPreviouslySynced(): Promise<boolean> {
  493         const lastSyncData = await this.getLastSyncUserData();
  494         return !!lastSyncData;
  495     }
  496 
  497     async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
  498         const handles = await this.userDataSyncStoreService.getAllRefs(this.resource);
  499         return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) }));
  500     }
  501 
  502     async getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
  503         const handles = await this.userDataSyncBackupStoreService.getAllRefs(this.resource);
  504         return handles.map(({ created, ref }) => ({ created, uri: this.toLocalBackupResource(ref) }));
  505     }
  506 
  507     private toRemoteBackupResource(ref: string): URI {
  508         return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote-backup', path: `/${this.resource}/${ref}` });
  509     }
  510 
  511     private toLocalBackupResource(ref: string): URI {
  512         return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${this.resource}/${ref}` });
  513     }
  514 
  515     async getMachineId({ uri }: ISyncResourceHandle): Promise<string | undefined> {
  516         const ref = basename(uri);
  517         if (isEqual(uri, this.toRemoteBackupResource(ref))) {
  518             const { content } = await this.getUserData(ref);
  519             if (content) {
  520                 const syncData = this.parseSyncData(content);
  521                 return syncData?.machineId;
  522             }
  523         }
  524         return undefined;
  525     }
  526 
  527     async resolveContent(uri: URI): Promise<string | null> {
  528         const ref = basename(uri);
  529         if (isEqual(uri, this.toRemoteBackupResource(ref))) {
  530             const { content } = await this.getUserData(ref);
  531             return content;
  532         }
  533         if (isEqual(uri, this.toLocalBackupResource(ref))) {
  534             return this.userDataSyncBackupStoreService.resolveContent(this.resource, ref);
  535         }
  536         return null;
  537     }
  538 
  539     protected async resolvePreviewContent(uri: URI): Promise<string | null> {
  540         const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null;
  541         if (syncPreview) {
  542             for (const resourcePreview of syncPreview.resourcePreviews) {
  543                 if (isEqual(resourcePreview.acceptedResource, uri)) {
  544                     return resourcePreview.acceptResult ? resourcePreview.acceptResult.content : null;
  545                 }
  546                 if (isEqual(resourcePreview.remoteResource, uri)) {
  547                     return resourcePreview.remoteContent;
  548                 }
  549                 if (isEqual(resourcePreview.localResource, uri)) {
  550                     return resourcePreview.localContent;
  551                 }
  552             }
  553         }
  554         return null;
  555     }
  556 
  557     async resetLocal(): Promise<void> {
  558         try {
  559             await this.fileService.del(this.lastSyncResource);
  560         } catch (e) { /* ignore */ }
  561     }
  562 
  563     private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, token: CancellationToken): Promise<ISyncResourcePreview> {
  564         const machineId = await this.currentMachineIdPromise;
  565         const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
  566 
  567         // For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine
  568         const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData;
  569         const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
  570 
  571         const resourcePreviews: IEditableResourcePreview[] = [];
  572         for (const resourcePreviewResult of resourcePreviewResults) {
  573             const acceptedResource = resourcePreviewResult.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
  574 
  575             /* No change -> Accept */
  576             if (resourcePreviewResult.localChange === Change.None && resourcePreviewResult.remoteChange === Change.None) {
  577                 resourcePreviews.push({
  578                     ...resourcePreviewResult,
  579                     acceptedResource,
  580                     acceptResult: { content: null, localChange: Change.None, remoteChange: Change.None },
  581                     mergeState: MergeState.Accepted
  582                 });
  583             }
  584 
  585             /* Changed -> Apply ? (Merge ? Conflict | Accept) : Preview */
  586             else {
  587                 /* Merge */
  588                 const mergeResult = apply ? await this.getMergeResult(resourcePreviewResult, token) : undefined;
  589                 if (token.isCancellationRequested) {
  590                     break;
  591                 }
  592                 await this.fileService.writeFile(resourcePreviewResult.previewResource, VSBuffer.fromString(mergeResult?.content || ''));
  593 
  594                 /* Conflict | Accept */
  595                 const acceptResult = mergeResult && !mergeResult.hasConflicts
  596                     /* Accept if merged and there are no conflicts */
  597                     ? await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, undefined, token)
  598                     : undefined;
  599 
  600                 resourcePreviews.push({
  601                     ...resourcePreviewResult,
  602                     acceptResult,
  603                     mergeState: mergeResult?.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview,
  604                     localChange: acceptResult ? acceptResult.localChange : mergeResult ? mergeResult.localChange : resourcePreviewResult.localChange,
  605                     remoteChange: acceptResult ? acceptResult.remoteChange : mergeResult ? mergeResult.remoteChange : resourcePreviewResult.remoteChange
  606                 });
  607             }
  608         }
  609 
  610         return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine };
  611     }
  612 
  613     async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
  614         try {
  615             const content = await this.fileService.readFile(this.lastSyncResource);
  616             const parsed = JSON.parse(content.value.toString());
  617             const userData: IUserData = parsed as IUserData;
  618             if (userData.content === null) {
  619                 return { ref: parsed.ref, syncData: null } as T;
  620             }
  621             const syncData: ISyncData = JSON.parse(userData.content);
  622 
  623             /* Check if syncData is of expected type. Return only if matches */
  624             if (isSyncData(syncData)) {
  625                 return { ...parsed, ...{ syncData, content: undefined } };
  626             }
  627 
  628         } catch (error) {
  629             if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) {
  630                 // log error always except when file does not exist
  631                 this.logService.error(error);
  632             }
  633         }
  634         return null;
  635     }
  636 
  637     protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary<any> = {}): Promise<void> {
  638         const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps };
  639         await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
  640     }
  641 
  642     async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise<IRemoteUserData> {
  643         const { ref, content } = await this.getUserData(lastSyncData);
  644         let syncData: ISyncData | null = null;
  645         if (content !== null) {
  646             syncData = this.parseSyncData(content);
  647         }
  648         return { ref, syncData };
  649     }
  650 
  651     protected parseSyncData(content: string): ISyncData {
  652         try {
  653             const syncData: ISyncData = JSON.parse(content);
  654             if (isSyncData(syncData)) {
  655                 return syncData;
  656             }
  657         } catch (error) {
  658             this.logService.error(error);
  659         }
  660         throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with the current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource);
  661     }
  662 
  663     private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> {
  664         if (isString(refOrLastSyncData)) {
  665             const content = await this.userDataSyncStoreService.resolveContent(this.resource, refOrLastSyncData);
  666             return { ref: refOrLastSyncData, content };
  667         } else {
  668             const lastSyncUserData: IUserData | null = refOrLastSyncData ? { ref: refOrLastSyncData.ref, content: refOrLastSyncData.syncData ? JSON.stringify(refOrLastSyncData.syncData) : null } : null;
  669             return this.userDataSyncStoreService.read(this.resource, lastSyncUserData, this.syncHeaders);
  670         }
  671     }
  672 
  673     protected async updateRemoteUserData(content: string, ref: string | null): Promise<IRemoteUserData> {
  674         const machineId = await this.currentMachineIdPromise;
  675         const syncData: ISyncData = { version: this.version, machineId, content };
  676         ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref, this.syncHeaders);
  677         return { ref, syncData };
  678     }
  679 
  680     protected async backupLocal(content: string): Promise<void> {
  681         const syncData: ISyncData = { version: this.version, content };
  682         return this.userDataSyncBackupStoreService.backup(this.resource, JSON.stringify(syncData));
  683     }
  684 
  685     async stop(): Promise<void> {
  686         if (this.status === SyncStatus.Idle) {
  687             return;
  688         }
  689 
  690         this.logService.trace(`${this.syncResourceLogLabel}: Stopping synchronizing ${this.resource.toLowerCase()}.`);
  691         if (this.syncPreviewPromise) {
  692             this.syncPreviewPromise.cancel();
  693             this.syncPreviewPromise = null;
  694         }
  695 
  696         this.updateConflicts([]);
  697         await this.clearPreviewFolder();
  698 
  699         this.setStatus(SyncStatus.Idle);
  700         this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
  701     }
  702 
  703     protected abstract readonly version: number;
  704     protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
  705     protected abstract getMergeResult(resourcePreview: IResourcePreview, token: CancellationToken): Promise<IMergeResult>;
  706     protected abstract getAcceptResult(resourcePreview: IResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult>;
  707     protected abstract applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, result: [IResourcePreview, IAcceptResult][], force: boolean): Promise<void>;
  708 }
  709 
  710 export interface IFileResourcePreview extends IResourcePreview {
  711     readonly fileContent: IFileContent | null;
  712 }
  713 
  714 export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
  715 
  716     constructor(
  717         protected readonly file: URI,
  718         resource: SyncResource,
  719         @IFileService fileService: IFileService,
  720         @IEnvironmentService environmentService: IEnvironmentService,
  721         @IStorageService storageService: IStorageService,
  722         @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
  723         @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
  724         @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
  725         @ITelemetryService telemetryService: ITelemetryService,
  726         @IUserDataSyncLogService logService: IUserDataSyncLogService,
  727         @IConfigurationService configurationService: IConfigurationService,
  728     ) {
  729         super(resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
  730         this._register(this.fileService.watch(dirname(file)));
  731         this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
  732     }
  733 
  734     protected async getLocalFileContent(): Promise<IFileContent | null> {
  735         try {
  736             return await this.fileService.readFile(this.file);
  737         } catch (error) {
  738             return null;
  739         }
  740     }
  741 
  742     protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null, force: boolean): Promise<void> {
  743         try {
  744             if (oldContent) {
  745                 // file exists already
  746                 await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), force ? undefined : oldContent);
  747             } else {
  748                 // file does not exist
  749                 await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: force });
  750             }
  751         } catch (e) {
  752             if ((e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) ||
  753                 (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
  754                 throw new UserDataSyncError(e.message, UserDataSyncErrorCode.LocalPreconditionFailed);
  755             } else {
  756                 throw e;
  757             }
  758         }
  759     }
  760 
  761     private onFileChanges(e: FileChangesEvent): void {
  762         if (!e.contains(this.file)) {
  763             return;
  764         }
  765         this.triggerLocalChange();
  766     }
  767 
  768 }
  769 
  770 export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
  771 
  772     constructor(
  773         file: URI,
  774         resource: SyncResource,
  775         @IFileService fileService: IFileService,
  776         @IEnvironmentService environmentService: IEnvironmentService,
  777         @IStorageService storageService: IStorageService,
  778         @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
  779         @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
  780         @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
  781         @ITelemetryService telemetryService: ITelemetryService,
  782         @IUserDataSyncLogService logService: IUserDataSyncLogService,
  783         @IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService,
  784         @IConfigurationService configurationService: IConfigurationService,
  785     ) {
  786         super(file, resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
  787     }
  788 
  789     protected hasErrors(content: string): boolean {
  790         const parseErrors: ParseError[] = [];
  791         parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
  792         return parseErrors.length > 0;
  793     }
  794 
  795     private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
  796     protected getFormattingOptions(): Promise<FormattingOptions> {
  797         if (!this._formattingOptions) {
  798             this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.file);
  799         }
  800         return this._formattingOptions;
  801     }
  802 
  803 }
  804 
  805 export abstract class AbstractInitializer {
  806 
  807     private readonly lastSyncResource: URI;
  808 
  809     constructor(
  810         readonly resource: SyncResource,
  811         @IEnvironmentService protected readonly environmentService: IEnvironmentService,
  812         @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
  813         @IFileService protected readonly fileService: IFileService,
  814     ) {
  815         this.lastSyncResource = getLastSyncResourceUri(this.resource, environmentService);
  816     }
  817 
  818     async initialize({ ref, content }: IUserData): Promise<void> {
  819         if (!content) {
  820             this.logService.info('Remote content does not exist.', this.resource);
  821             return;
  822         }
  823 
  824         const syncData = this.parseSyncData(content);
  825         if (!syncData) {
  826             return;
  827         }
  828 
  829         const isPreviouslySynced = await this.fileService.exists(this.lastSyncResource);
  830         if (isPreviouslySynced) {
  831             this.logService.info('Remote content does not exist.', this.resource);
  832             return;
  833         }
  834 
  835         try {
  836             await this.doInitialize({ ref, syncData });
  837         } catch (error) {
  838             this.logService.error(error);
  839         }
  840     }
  841 
  842     private parseSyncData(content: string): ISyncData | undefined {
  843         try {
  844             const syncData: ISyncData = JSON.parse(content);
  845             if (isSyncData(syncData)) {
  846                 return syncData;
  847             }
  848         } catch (error) {
  849             this.logService.error(error);
  850         }
  851         this.logService.info('Cannot parse sync data as it is not compatible with the current version.', this.resource);
  852         return undefined;
  853     }
  854 
  855     protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary<any> = {}): Promise<void> {
  856         const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps };
  857         await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
  858     }
  859 
  860     protected abstract doInitialize(remoteUserData: IRemoteUserData): Promise<void>;
  861 
  862 }