abstractSynchronizer.ts 10.7 KB
Newer Older
S
Sandeep Somavarapu 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { Disposable } from 'vs/base/common/lifecycle';
7
import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
S
Sandeep Somavarapu 已提交
8 9
import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
10
import { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
S
Sandeep Somavarapu 已提交
11
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
S
Sandeep Somavarapu 已提交
12
import { joinPath, dirname } from 'vs/base/common/resources';
S
Sandeep Somavarapu 已提交
13
import { toLocalISOString } from 'vs/base/common/date';
14
import { ThrottledDelayer, CancelablePromise } from 'vs/base/common/async';
S
Sandeep Somavarapu 已提交
15
import { Emitter, Event } from 'vs/base/common/event';
16
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
17 18
import { ParseError, parse } from 'vs/base/common/json';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
19 20 21 22

type SyncConflictsClassification = {
	source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
S
Sandeep Somavarapu 已提交
23 24 25

export abstract class AbstractSynchroniser extends Disposable {

26
	protected readonly syncFolder: URI;
S
Sandeep Somavarapu 已提交
27 28
	private cleanUpDelayer: ThrottledDelayer<void>;

S
Sandeep Somavarapu 已提交
29 30 31 32 33 34 35 36 37 38
	private _status: SyncStatus = SyncStatus.Idle;
	get status(): SyncStatus { return this._status; }
	private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
	readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;

	protected readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
	readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;

	protected readonly lastSyncResource: URI;

S
Sandeep Somavarapu 已提交
39
	constructor(
40
		readonly source: SyncSource,
S
Sandeep Somavarapu 已提交
41
		@IFileService protected readonly fileService: IFileService,
S
Sandeep Somavarapu 已提交
42 43
		@IEnvironmentService environmentService: IEnvironmentService,
		@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
44
		@ITelemetryService private readonly telemetryService: ITelemetryService,
45
		@IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
S
Sandeep Somavarapu 已提交
46 47
	) {
		super();
S
Sandeep Somavarapu 已提交
48
		this.syncFolder = joinPath(environmentService.userDataSyncHome, source);
S
Sandeep Somavarapu 已提交
49
		this.lastSyncResource = joinPath(this.syncFolder, `.lastSync${source}.json`);
S
Sandeep Somavarapu 已提交
50 51 52
		this.cleanUpDelayer = new ThrottledDelayer(50);
	}

S
Sandeep Somavarapu 已提交
53 54
	protected setStatus(status: SyncStatus): void {
		if (this._status !== status) {
55
			const oldStatus = this._status;
S
Sandeep Somavarapu 已提交
56 57
			this._status = status;
			this._onDidChangStatus.fire(status);
58 59 60 61 62 63 64 65
			if (status === SyncStatus.HasConflicts) {
				// Log to telemetry when there is a sync conflict
				this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsDetected', { source: this.source });
			}
			if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) {
				// Log to telemetry when conflicts are resolved
				this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsResolved', { source: this.source });
			}
S
Sandeep Somavarapu 已提交
66 67 68
		}
	}

69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
	async sync(): Promise<void> {
		if (!this.enabled) {
			this.logService.info(`${this.source}: Skipping synchronizing ${this.source.toLowerCase()} as it is disabled.`);
			return;
		}
		if (this.status !== SyncStatus.Idle) {
			this.logService.info(`${this.source}: Skipping synchronizing ${this.source.toLowerCase()} as it is running already.`);
			return;
		}

		this.logService.trace(`${this.source}: Started synchronizing ${this.source.toLowerCase()}...`);
		this.setStatus(SyncStatus.Syncing);
		return this.doSync();
	}

S
Sandeep Somavarapu 已提交
84 85 86 87 88 89
	async hasPreviouslySynced(): Promise<boolean> {
		const lastSyncData = await this.getLastSyncUserData();
		return !!lastSyncData;
	}

	async hasRemoteData(): Promise<boolean> {
S
Sandeep Somavarapu 已提交
90 91
		const lastSyncData = await this.getLastSyncUserData();
		const remoteUserData = await this.getRemoteUserData(lastSyncData);
S
Sandeep Somavarapu 已提交
92 93 94
		return remoteUserData.content !== null;
	}

95 96 97 98 99 100
	async getRemoteContent(): Promise<string | null> {
		const lastSyncData = await this.getLastSyncUserData();
		const remoteUserData = await this.getRemoteUserData(lastSyncData);
		return remoteUserData.content;
	}

S
Sandeep Somavarapu 已提交
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
	async resetLocal(): Promise<void> {
		try {
			await this.fileService.del(this.lastSyncResource);
		} catch (e) { /* ignore */ }
	}

	protected async getLastSyncUserData<T extends IUserData>(): Promise<T | null> {
		try {
			const content = await this.fileService.readFile(this.lastSyncResource);
			return JSON.parse(content.value.toString());
		} catch (error) {
			return null;
		}
	}

	protected async updateLastSyncUserData<T extends IUserData>(lastSyncUserData: T): Promise<void> {
		await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
	}

S
Sandeep Somavarapu 已提交
120
	protected async getRemoteUserData(lastSyncData: IUserData | null): Promise<IUserData> {
121
		return this.userDataSyncStoreService.read(this.remoteDataResourceKey, lastSyncData, this.source);
S
Sandeep Somavarapu 已提交
122 123 124
	}

	protected async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
125
		return this.userDataSyncStoreService.write(this.remoteDataResourceKey, content, ref, this.source);
S
Sandeep Somavarapu 已提交
126 127
	}

S
Sandeep Somavarapu 已提交
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
	protected async backupLocal(content: VSBuffer): Promise<void> {
		const resource = joinPath(this.syncFolder, toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''));
		await this.fileService.writeFile(resource, content);
		this.cleanUpDelayer.trigger(() => this.cleanUpBackup());
	}

	private async cleanUpBackup(): Promise<void> {
		const stat = await this.fileService.resolve(this.syncFolder);
		if (stat.children) {
			const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}$/.test(stat.name)).sort();
			const toDelete = all.slice(0, Math.max(0, all.length - 9));
			await Promise.all(toDelete.map(stat => this.fileService.del(stat.resource)));
		}
	}

143
	protected abstract readonly enabled: boolean;
144 145 146 147 148 149 150 151 152 153 154 155
	protected abstract readonly remoteDataResourceKey: string;
	protected abstract doSync(): Promise<void>;
}

export interface IFileSyncPreviewResult {
	readonly fileContent: IFileContent | null;
	readonly remoteUserData: IUserData;
	readonly lastSyncUserData: IUserData | null;
	readonly content: string | null;
	readonly hasLocalChanged: boolean;
	readonly hasRemoteChanged: boolean;
	readonly hasConflicts: boolean;
S
Sandeep Somavarapu 已提交
156 157 158 159
}

export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {

160 161
	protected syncPreviewResultPromise: CancelablePromise<IFileSyncPreviewResult> | null = null;

S
Sandeep Somavarapu 已提交
162 163
	constructor(
		protected readonly file: URI,
164
		source: SyncSource,
S
Sandeep Somavarapu 已提交
165 166 167
		@IFileService fileService: IFileService,
		@IEnvironmentService environmentService: IEnvironmentService,
		@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
168
		@ITelemetryService telemetryService: ITelemetryService,
169
		@IUserDataSyncLogService logService: IUserDataSyncLogService,
S
Sandeep Somavarapu 已提交
170
	) {
171
		super(source, fileService, environmentService, userDataSyncStoreService, telemetryService, logService);
S
Sandeep Somavarapu 已提交
172
		this._register(this.fileService.watch(dirname(file)));
173
		this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
S
Sandeep Somavarapu 已提交
174 175
	}

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
	async stop(): Promise<void> {
		this.cancel();
		this.logService.trace(`${this.source}: Stopped synchronizing ${this.source.toLowerCase()}.`);
		await this.fileService.del(this.conflictsPreviewResource);
		this.setStatus(SyncStatus.Idle);
	}

	async getRemoteContent(preview?: boolean): Promise<string | null> {
		if (preview) {
			if (this.syncPreviewResultPromise) {
				const result = await this.syncPreviewResultPromise;
				return result.remoteUserData ? result.remoteUserData.content : null;
			}
		}
		return super.getRemoteContent();
	}

S
Sandeep Somavarapu 已提交
193 194 195 196 197 198 199 200 201
	protected async getLocalFileContent(): Promise<IFileContent | null> {
		try {
			return await this.fileService.readFile(this.file);
		} catch (error) {
			return null;
		}
	}

	protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
		try {
			if (oldContent) {
				// file exists already
				await this.backupLocal(oldContent.value);
				await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent);
			} else {
				// file does not exist
				await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
			}
		} catch (e) {
			if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
				(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
				throw new UserDataSyncError(e.message, UserDataSyncErrorCode.NewLocal);
			} else {
				throw e;
			}
		}
	}

	private onFileChanges(e: FileChangesEvent): void {
		if (!e.contains(this.file)) {
			return;
		}
		if (!this.enabled) {
			return;
		}
		// Sync again if local file has changed and current status is in conflicts
		if (this.status === SyncStatus.HasConflicts) {
			this.cancel();
			this.doSync();
S
Sandeep Somavarapu 已提交
232
		}
233 234 235 236 237
		// Otherwise fire change event
		else {
			this._onDidChangeLocal.fire();
		}

S
Sandeep Somavarapu 已提交
238 239
	}

240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
	protected cancel(): void {
		if (this.syncPreviewResultPromise) {
			this.syncPreviewResultPromise.cancel();
			this.syncPreviewResultPromise = null;
		}
	}

	protected abstract readonly conflictsPreviewResource: URI;
}

export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {

	constructor(
		file: URI,
		source: SyncSource,
		@IFileService fileService: IFileService,
		@IEnvironmentService environmentService: IEnvironmentService,
		@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
		@ITelemetryService telemetryService: ITelemetryService,
		@IUserDataSyncLogService logService: IUserDataSyncLogService,
		@IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService,
	) {
		super(file, source, fileService, environmentService, userDataSyncStoreService, telemetryService, logService);
	}

	protected hasErrors(content: string): boolean {
		const parseErrors: ParseError[] = [];
		parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
		return parseErrors.length > 0;
	}

	private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
	protected getFormattingOptions(): Promise<FormattingOptions> {
		if (!this._formattingOptions) {
			this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.file);
		}
		return this._formattingOptions;
	}

S
Sandeep Somavarapu 已提交
279
}