Skip to content

Commit a25e2db

Browse files
committed
microsoft#100346 introduce manual sync task
1 parent 6a4866a commit a25e2db

6 files changed

Lines changed: 284 additions & 23 deletions

File tree

src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat
238238
server.registerChannel('userDataSyncAccount', authTokenChannel);
239239

240240
const userDataSyncService = accessor.get(IUserDataSyncService);
241-
const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService, logService);
241+
const userDataSyncChannel = new UserDataSyncChannel(server, userDataSyncService, logService);
242242
server.registerChannel('userDataSync', userDataSyncChannel);
243243

244244
const userDataAutoSync = instantiationService2.createInstance(UserDataAutoSyncService);

src/vs/platform/userDataSync/common/abstractSynchronizer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ export abstract class AbstractSynchroniser extends Disposable {
210210
await this._sync(manifest, true, headers);
211211
}
212212

213+
async preview(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
214+
return this._sync(manifest, false, headers);
215+
}
216+
213217
private async _sync(manifest: IUserDataManifest | null, apply: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null> {
214218
try {
215219
this.syncHeaders = { ...headers };

src/vs/platform/userDataSync/common/userDataSync.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ export interface IUserDataSynchroniser {
325325
pull(): Promise<void>;
326326
push(): Promise<void>;
327327
sync(manifest: IUserDataManifest | null, headers: IHeaders): Promise<void>;
328+
preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise<ISyncResourcePreview | null>;
328329
replace(uri: URI): Promise<boolean>;
329330
stop(): Promise<void>;
330331

@@ -363,6 +364,16 @@ export interface ISyncTask {
363364
stop(): Promise<void>;
364365
}
365366

367+
export interface IManualSyncTask {
368+
readonly manifest: IUserDataManifest | null;
369+
preview(): Promise<[SyncResource, ISyncResourcePreview][]>;
370+
merge(): Promise<[SyncResource, ISyncResourcePreview][]>;
371+
pull(): Promise<void>;
372+
push(): Promise<void>;
373+
accept(uri: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]>;
374+
stop(): Promise<void>;
375+
}
376+
366377
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
367378
export interface IUserDataSyncService {
368379
_serviceBrand: any;
@@ -381,12 +392,14 @@ export interface IUserDataSyncService {
381392
readonly onDidChangeLastSyncTime: Event<number>;
382393

383394
createSyncTask(): Promise<ISyncTask>;
395+
createManualSyncTask(): Promise<IManualSyncTask>;
384396

385397
pull(): Promise<void>;
386398
replace(uri: URI): Promise<void>;
387399
reset(): Promise<void>;
388400
resetLocal(): Promise<void>;
389401

402+
hasLocalData(): Promise<boolean>;
390403
isFirstTimeSyncingWithAnotherMachine(): Promise<boolean>;
391404
hasPreviouslySynced(): Promise<boolean>;
392405
resolveContent(resource: URI): Promise<string | null>;

src/vs/platform/userDataSync/common/userDataSyncIpc.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
6+
import { IServerChannel, IChannel, IPCServer } from 'vs/base/parts/ipc/common/ipc';
77
import { Event, Emitter } from 'vs/base/common/event';
8-
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync';
8+
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
99
import { URI } from 'vs/base/common/uri';
1010
import { IStringDictionary } from 'vs/base/common/collections';
1111
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
@@ -17,7 +17,7 @@ import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/use
1717

1818
export class UserDataSyncChannel implements IServerChannel {
1919

20-
constructor(private readonly service: IUserDataSyncService, private readonly logService: ILogService) { }
20+
constructor(private server: IPCServer, private readonly service: IUserDataSyncService, private readonly logService: ILogService) { }
2121

2222
listen(_: unknown, event: string): Event<any> {
2323
switch (event) {
@@ -44,11 +44,15 @@ export class UserDataSyncChannel implements IServerChannel {
4444
private _call(context: any, command: string, args?: any): Promise<any> {
4545
switch (command) {
4646
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]);
47+
48+
case 'createManualSyncTask': return this.createManualSyncTask();
49+
4750
case 'pull': return this.service.pull();
4851
case 'replace': return this.service.replace(URI.revive(args[0]));
4952
case 'reset': return this.service.reset();
5053
case 'resetLocal': return this.service.resetLocal();
5154
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
55+
case 'hasLocalData': return this.service.hasLocalData();
5256
case 'isFirstTimeSyncingWithAnotherMachine': return this.service.isFirstTimeSyncingWithAnotherMachine();
5357
case 'acceptPreviewContent': return this.service.acceptPreviewContent(URI.revive(args[0]), args[1]);
5458
case 'resolveContent': return this.service.resolveContent(URI.revive(args[0]));
@@ -59,6 +63,37 @@ export class UserDataSyncChannel implements IServerChannel {
5963
}
6064
throw new Error('Invalid call');
6165
}
66+
67+
private taskCounter = 1;
68+
private async createManualSyncTask(): Promise<{ initialData: { manifest: IUserDataManifest | null }, channelName: string }> {
69+
const manualSyncTask = await this.service.createManualSyncTask();
70+
const manualSyncTaskChannel = new ManualSyncTaskChannel(manualSyncTask);
71+
const channelName = `manualSyncTask-${this.taskCounter++}`;
72+
this.server.registerChannel(channelName, manualSyncTaskChannel);
73+
return { initialData: { manifest: manualSyncTask.manifest }, channelName };
74+
}
75+
}
76+
77+
class ManualSyncTaskChannel implements IServerChannel {
78+
79+
constructor(private readonly manualSyncTask: IManualSyncTask) { }
80+
81+
listen(_: unknown, event: string): Event<any> {
82+
throw new Error(`Event not found: ${event}`);
83+
}
84+
85+
async call(context: any, command: string, args?: any): Promise<any> {
86+
switch (command) {
87+
case 'preview': return this.manualSyncTask.preview();
88+
case 'accept': return this.manualSyncTask.accept(URI.revive(args[0]), args[1]);
89+
case 'merge': return this.manualSyncTask.merge();
90+
case 'pull': return this.manualSyncTask.pull();
91+
case 'push': return this.manualSyncTask.push();
92+
case 'stop': return this.manualSyncTask.stop();
93+
}
94+
throw new Error('Invalid call');
95+
}
96+
6297
}
6398

6499
export class UserDataAutoSyncChannel implements IServerChannel {

src/vs/platform/userDataSync/common/userDataSyncService.ts

Lines changed: 175 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask, Change, IResourcePreview } from 'vs/platform/userDataSync/common/userDataSync';
6+
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask, Change, IResourcePreview, IManualSyncTask, ISyncResourcePreview } from 'vs/platform/userDataSync/common/userDataSync';
77
import { Disposable } from 'vs/base/common/lifecycle';
88
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
99
import { Emitter, Event } from 'vs/base/common/event';
@@ -22,6 +22,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
2222
import { IHeaders } from 'vs/base/parts/request/common/request';
2323
import { generateUuid } from 'vs/base/common/uuid';
2424
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
25+
import { isPromiseCanceledError } from 'vs/base/common/errors';
2526

2627
type SyncErrorClassification = {
2728
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -131,7 +132,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
131132
}
132133

133134
async createSyncTask(): Promise<ISyncTask> {
134-
this.telemetryService.publicLog2('sync/getmanifest');
135+
await this.checkEnablement();
136+
135137
const executionId = generateUuid();
136138
let manifest: IUserDataManifest | null;
137139
try {
@@ -164,10 +166,26 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
164166
};
165167
}
166168

167-
private recoveredSettings: boolean = false;
168-
private async sync(manifest: IUserDataManifest | null, executionId: string, token: CancellationToken): Promise<void> {
169+
async createManualSyncTask(): Promise<IManualSyncTask> {
169170
await this.checkEnablement();
170171

172+
const executionId = generateUuid();
173+
const syncHeaders: IHeaders = { 'X-Execution-Id': executionId };
174+
let manifest: IUserDataManifest | null;
175+
try {
176+
manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
177+
} catch (error) {
178+
if (error instanceof UserDataSyncError) {
179+
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
180+
}
181+
throw error;
182+
}
183+
184+
return new ManualSyncTask(manifest, syncHeaders, this.synchronisers, this.logService);
185+
}
186+
187+
private recoveredSettings: boolean = false;
188+
private async sync(manifest: IUserDataManifest | null, executionId: string, token: CancellationToken): Promise<void> {
171189
if (!this.recoveredSettings) {
172190
await this.settingsSynchroniser.recoverSettings();
173191
this.recoveredSettings = true;
@@ -276,6 +294,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
276294
return this.getSynchroniser(resource).getMachineId(syncResourceHandle);
277295
}
278296

297+
async hasLocalData(): Promise<boolean> {
298+
// skip global state synchronizer
299+
const synchronizers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser];
300+
for (const synchroniser of synchronizers) {
301+
if (await synchroniser.hasLocalData()) {
302+
return true;
303+
}
304+
}
305+
return false;
306+
}
307+
279308
async isFirstTimeSyncingWithAnotherMachine(): Promise<boolean> {
280309
await this.checkEnablement();
281310

@@ -414,22 +443,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
414443

415444
private computeConflicts(): SyncResourceConflicts[] {
416445
return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)
417-
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts.map(r => this.toStrictResourcePreview(r)) }));
418-
}
419-
420-
private toStrictResourcePreview(resourcePreview: IResourcePreview): IResourcePreview {
421-
return {
422-
localResource: resourcePreview.localResource,
423-
previewResource: resourcePreview.previewResource,
424-
remoteResource: resourcePreview.remoteResource,
425-
localChange: resourcePreview.localChange,
426-
remoteChange: resourcePreview.remoteChange,
427-
hasConflicts: resourcePreview.hasConflicts,
428-
};
446+
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts.map(toStrictResourcePreview) }));
429447
}
430448

431449
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
432-
return this.synchronisers.filter(s => s.resource === source)[0];
450+
return this.synchronisers.find(s => s.resource === source)!;
433451
}
434452

435453
private async checkEnablement(): Promise<void> {
@@ -439,3 +457,143 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
439457
}
440458

441459
}
460+
461+
class ManualSyncTask implements IManualSyncTask {
462+
463+
private previewsPromise: CancelablePromise<[SyncResource, ISyncResourcePreview][]> | undefined;
464+
private previews: [SyncResource, ISyncResourcePreview][] | undefined;
465+
466+
constructor(readonly manifest: IUserDataManifest | null,
467+
private readonly syncHeaders: IHeaders,
468+
private readonly synchronisers: IUserDataSynchroniser[],
469+
private readonly logService: IUserDataSyncLogService,
470+
) { }
471+
472+
async preview(): Promise<[SyncResource, ISyncResourcePreview][]> {
473+
if (!this.previewsPromise) {
474+
this.previewsPromise = createCancelablePromise(token => this.getPreviews(token));
475+
}
476+
this.previews = await this.previewsPromise;
477+
return this.previews;
478+
}
479+
480+
async accept(resource: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]> {
481+
if (!this.previews) {
482+
throw new Error('You need to create preview before applying');
483+
}
484+
const index = this.previews.findIndex(([, preview]) => preview.resourcePreviews.some(({ localResource, previewResource, remoteResource }) =>
485+
isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource)));
486+
if (index !== -1) {
487+
const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!;
488+
/* force only if the resource is local or remote resource */
489+
const force = this.previews![index][1].resourcePreviews.some(({ localResource, remoteResource }) => isEqual(resource, localResource) || isEqual(resource, remoteResource));
490+
const preview = await synchroniser.acceptPreviewContent(resource, content, force, this.syncHeaders);
491+
preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1);
492+
}
493+
return this.previews;
494+
}
495+
496+
async merge(): Promise<[SyncResource, ISyncResourcePreview][]> {
497+
if (!this.previews) {
498+
throw new Error('You need to create preview before applying');
499+
}
500+
const previews: [SyncResource, ISyncResourcePreview][] = [];
501+
for (const [syncResource, preview] of this.previews) {
502+
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
503+
let newPreview: ISyncResourcePreview | null = null;
504+
for (const resourcePreview of preview.resourcePreviews) {
505+
/* merge only if there are no conflicts */
506+
if (!resourcePreview.hasConflicts) {
507+
const content = await synchroniser.resolveContent(resourcePreview.previewResource) || '';
508+
newPreview = await synchroniser.acceptPreviewContent(resourcePreview.previewResource, content, false, this.syncHeaders);
509+
}
510+
}
511+
if (newPreview) {
512+
previews.push(this.toSyncResourcePreview(syncResource, newPreview));
513+
}
514+
}
515+
this.previews = previews;
516+
return this.previews;
517+
}
518+
519+
async pull(): Promise<void> {
520+
if (!this.previews) {
521+
throw new Error('You need to create preview before applying');
522+
}
523+
for (const [syncResource, preview] of this.previews) {
524+
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
525+
for (const resourcePreview of preview.resourcePreviews) {
526+
const content = await synchroniser.resolveContent(resourcePreview.remoteResource) || '';
527+
await synchroniser.acceptPreviewContent(resourcePreview.remoteResource, content, true, this.syncHeaders);
528+
}
529+
}
530+
this.previews = [];
531+
}
532+
533+
async push(): Promise<void> {
534+
if (!this.previews) {
535+
throw new Error('You need to create preview before applying');
536+
}
537+
for (const [syncResource, preview] of this.previews) {
538+
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
539+
for (const resourcePreview of preview.resourcePreviews) {
540+
const content = await synchroniser.resolveContent(resourcePreview.localResource) || '';
541+
await synchroniser.acceptPreviewContent(resourcePreview.localResource, content, true, this.syncHeaders);
542+
}
543+
}
544+
this.previews = [];
545+
}
546+
547+
async stop(): Promise<void> {
548+
if (this.previewsPromise) {
549+
this.previewsPromise.cancel();
550+
this.previewsPromise = undefined;
551+
}
552+
this.previews = undefined;
553+
for (const synchroniser of this.synchronisers) {
554+
try {
555+
await synchroniser.stop();
556+
} catch (error) {
557+
if (!isPromiseCanceledError(error)) {
558+
this.logService.error(error);
559+
}
560+
}
561+
}
562+
}
563+
564+
private async getPreviews(token: CancellationToken): Promise<[SyncResource, ISyncResourcePreview][]> {
565+
const result: [SyncResource, ISyncResourcePreview][] = [];
566+
for (const synchroniser of this.synchronisers) {
567+
if (token.isCancellationRequested) {
568+
return [];
569+
}
570+
const preview = await synchroniser.preview(this.manifest, this.syncHeaders);
571+
if (preview) {
572+
result.push(this.toSyncResourcePreview(synchroniser.resource, preview));
573+
}
574+
}
575+
return result;
576+
}
577+
578+
private toSyncResourcePreview(syncResource: SyncResource, preview: ISyncResourcePreview): [SyncResource, ISyncResourcePreview] {
579+
return [
580+
syncResource,
581+
{
582+
isLastSyncFromCurrentMachine: preview.isLastSyncFromCurrentMachine,
583+
resourcePreviews: preview.resourcePreviews.map(toStrictResourcePreview)
584+
}
585+
];
586+
}
587+
588+
}
589+
590+
function toStrictResourcePreview(resourcePreview: IResourcePreview): IResourcePreview {
591+
return {
592+
localResource: resourcePreview.localResource,
593+
previewResource: resourcePreview.previewResource,
594+
remoteResource: resourcePreview.remoteResource,
595+
localChange: resourcePreview.localChange,
596+
remoteChange: resourcePreview.remoteChange,
597+
hasConflicts: resourcePreview.hasConflicts,
598+
};
599+
}

0 commit comments

Comments
 (0)