提交 5ca3e09e 编写于 作者: S Sandeep Somavarapu

Fix #99102

上级 208d556b
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync';
......@@ -13,6 +13,10 @@ import { isPromiseCanceledError } from 'vs/base/common/errors';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IUserDataSyncMachine, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { PlatformToString, isWeb, Platform, platform } from 'vs/base/common/platform';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
import { IProductService } from 'vs/platform/product/common/productService';
type AutoSyncClassification = {
sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
......@@ -23,6 +27,7 @@ type AutoSyncEnablementClassification = {
};
const enablementKey = 'sync.enable';
const disableMachineEventuallyKey = 'sync.disableMachineEventually';
export class UserDataAutoSyncEnablementService extends Disposable {
......@@ -80,6 +85,8 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IUserDataSyncAccountService private readonly authTokenService: IUserDataSyncAccountService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
@IProductService private readonly productService: IProductService,
@IStorageService storageService: IStorageService,
@IEnvironmentService environmentService: IEnvironmentService
) {
......@@ -88,6 +95,14 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
if (userDataSyncStoreService.userDataSyncStore) {
this.updateAutoSync();
// Update machine if sync is enabled
if (this.isEnabled()) {
this.updateMachine(true);
} else if (this.hasToDisableMachineEventually()) {
this.disableMachineEventually();
}
this._register(authTokenService.onDidChangeAccount(() => this.updateAutoSync()));
this._register(Event.debounce<string, string[]>(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false)));
this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false)));
......@@ -128,6 +143,8 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
}
async turnOn(pullFirst: boolean): Promise<void> {
await this.updateMachine(true);
if (pullFirst) {
await this.userDataSyncService.pull();
} else {
......@@ -137,8 +154,27 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
this.setEnablement(true);
}
async turnOff(): Promise<void> {
this.setEnablement(false);
async turnOff(everywhere: boolean, softTurnOffOnError?: boolean, donotDisableMachine?: boolean): Promise<void> {
try {
if (!donotDisableMachine) {
await this.updateMachine(false);
}
this.setEnablement(false);
if (everywhere) {
this.telemetryService.publicLog2('sync/turnOffEveryWhere');
await this.userDataSyncService.reset();
} else {
await this.userDataSyncService.resetLocal();
}
} catch (error) {
if (softTurnOffOnError) {
this.logService.error(error);
this.setEnablement(false);
} else {
throw error;
}
}
}
private setEnablement(enabled: boolean): void {
......@@ -149,6 +185,44 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
}
}
private async updateMachine(enable: boolean): Promise<void> {
if (!this.authTokenService.account) {
return;
}
const machines = await this.userDataSyncMachinesService.getMachines();
const currentMachine = machines.find(machine => machine.isCurrent);
if (enable) {
this.stopDisableMachineEventually();
// Add or enable current machine
if (!currentMachine) {
const name = this.computeDefaultMachineName(machines);
await this.userDataSyncMachinesService.addCurrentMachine(name);
this.logService.debug('Auto Sync: Added current machine to sync');
} else if (currentMachine.disabled) {
await this.userDataSyncMachinesService.setEnablement(currentMachine.id, true);
this.logService.debug('Auto Sync: Enabled current machine to sync');
}
} else if (currentMachine && !currentMachine.disabled) {
await this.userDataSyncMachinesService.setEnablement(currentMachine.id, false);
this.logService.debug('Auto Sync: Disabled current machine to sync');
}
}
private computeDefaultMachineName(machines: IUserDataSyncMachine[]): string {
const namePrefix = `${this.productService.nameLong} (${PlatformToString(isWeb ? Platform.Web : platform)})`;
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`);
let nameIndex = 0;
for (const machine of machines) {
const matches = nameRegEx.exec(machine.name);
const index = matches ? parseInt(matches[1]) : 0;
nameIndex = index > nameIndex ? index : nameIndex;
}
return `${namePrefix} #${nameIndex + 1}`;
}
private async onDidFinishSync(error: Error | undefined): Promise<void> {
if (!error) {
// Sync finished without errors
......@@ -159,12 +233,12 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
// Error while syncing
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff || userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
this.logService.info('Auto Sync: Sync is turned off in the cloud.');
await this.userDataSyncService.resetLocal();
this.turnOff();
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
} else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests) {
this.turnOff();
} else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests || userDataSyncError.code === UserDataSyncErrorCode.TooManyRequests) {
await this.turnOff(false, true /* force soft turnoff on error */,
true /* do not disable machine because disabling a machine makes request to server and can fail with TooManyRequests */);
this.disableMachineEventually();
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
} else {
this.logService.error(userDataSyncError);
......@@ -173,6 +247,31 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
this._onError.fire(userDataSyncError);
}
private async disableMachineEventually(): Promise<void> {
this.storageService.store(disableMachineEventuallyKey, true, StorageScope.GLOBAL);
await timeout(1000 * 60 * 10);
// Return if got stopped meanwhile.
if (!this.hasToDisableMachineEventually()) {
return;
}
this.stopDisableMachineEventually();
// disable only if sync is disabled
if (!this.isEnabled()) {
return this.updateMachine(false);
}
}
private hasToDisableMachineEventually(): boolean {
return this.storageService.getBoolean(disableMachineEventuallyKey, StorageScope.GLOBAL, false);
}
private stopDisableMachineEventually(): void {
this.storageService.remove(disableMachineEventuallyKey, StorageScope.GLOBAL);
}
private sources: string[] = [];
async triggerSync(sources: string[], skipIfSyncedRecently: boolean): Promise<void> {
if (this.autoSync.value === undefined) {
......
......@@ -381,7 +381,7 @@ export interface IUserDataAutoSyncService {
isEnabled(): boolean;
canToggleEnablement(): boolean;
turnOn(pullFirst: boolean): Promise<void>;
turnOff(): Promise<void>;
turnOff(everywhere: boolean): Promise<void>;
triggerSync(sources: string[], hasToLimitSync: boolean): Promise<void>;
}
......
......@@ -77,7 +77,7 @@ export class UserDataAutoSyncChannel implements IServerChannel {
switch (command) {
case 'triggerSync': return this.service.triggerSync(args[0], args[1]);
case 'turnOn': return this.service.turnOn(args[0]);
case 'turnOff': return this.service.turnOff();
case 'turnOff': return this.service.turnOff(args[0]);
}
throw new Error('Invalid call');
}
......
......@@ -19,10 +19,7 @@ import { URI } from 'vs/base/common/uri';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { isEqual } from 'vs/base/common/resources';
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { IProductService } from 'vs/platform/product/common/productService';
import { platform, PlatformToString, isWeb, Platform } from 'vs/base/common/platform';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { CancellationToken } from 'vs/base/common/cancellation';
import { generateUuid } from 'vs/base/common/uuid';
import { IHeaders } from 'vs/base/parts/request/common/request';
......@@ -74,7 +71,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
@IProductService private readonly productService: IProductService
) {
super();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
......@@ -175,17 +171,14 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined);
const currentMachine = machines.find(machine => machine.isCurrent);
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
const currentMachine = machines.find(machine => machine.isCurrent);
// Check if sync was turned off from other machine
if (currentMachine?.disabled) {
// Unset the current machine
await this.userDataSyncMachinesService.removeCurrentMachine(manifest || undefined);
// Throw TurnedOff error
throw new UserDataSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
}
......@@ -223,12 +216,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return;
}
// Add current machine
if (!currentMachine) {
const name = this.computeDefaultMachineName(machines);
await this.userDataSyncMachinesService.addCurrentMachine(name, manifest || undefined);
}
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
......@@ -346,16 +333,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
async reset(): Promise<void> {
await this.checkEnablement();
await this.resetRemote();
await this.resetLocal(true);
await this.resetLocal();
}
async resetLocal(donotUnsetMachine?: boolean): Promise<void> {
async resetLocal(): Promise<void> {
await this.checkEnablement();
this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL);
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL);
if (!donotUnsetMachine) {
await this.userDataSyncMachinesService.removeCurrentMachine();
}
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.resetLocal();
......@@ -455,20 +439,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts }));
}
private computeDefaultMachineName(machines: IUserDataSyncMachine[]): string {
const namePrefix = `${this.productService.nameLong} (${PlatformToString(isWeb ? Platform.Web : platform)})`;
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`);
let nameIndex = 0;
for (const machine of machines) {
const matches = nameRegEx.exec(machine.name);
const index = matches ? parseInt(matches[1]) : 0;
nameIndex = index > nameIndex ? index : nameIndex;
}
return `${namePrefix} #${nameIndex + 1}`;
}
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
return this.synchronisers.filter(s => s.resource === source)[0];
}
......
......@@ -11,6 +11,8 @@ import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/use
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { IProductService } from 'vs/platform/product/common/productService';
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
......@@ -22,10 +24,12 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IUserDataSyncAccountService authTokenService: IUserDataSyncAccountService,
@ITelemetryService telemetryService: ITelemetryService,
@IUserDataSyncMachinesService userDataSyncMachinesService: IUserDataSyncMachinesService,
@IProductService productService: IProductService,
@IStorageService storageService: IStorageService,
@IEnvironmentService environmentService: IEnvironmentService,
) {
super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, storageService, environmentService);
super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, productService, storageService, environmentService);
this._register(Event.debounce<string, string[]>(Event.any<string>(
Event.map(electronService.onWindowFocus, () => 'windowFocus'),
......
......@@ -36,8 +36,11 @@ suite('UserDataAutoSyncService', () => {
// Trigger auto sync with settings change
await testObject.triggerSync([SyncResource.Settings], false);
// Make sure only one request is made
assert.deepEqual(target.requests, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
// Filter out machine requests
const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`));
// Make sure only one manifest request is made
assert.deepEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
});
test('test auto sync with sync resource change triggers sync for every change', async () => {
......@@ -57,7 +60,10 @@ suite('UserDataAutoSyncService', () => {
await testObject.triggerSync([SyncResource.Settings], false);
}
assert.deepEqual(target.requests, [
// Filter out machine requests
const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`));
assert.deepEqual(actual, [
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }
]);
......@@ -78,8 +84,11 @@ suite('UserDataAutoSyncService', () => {
// Trigger auto sync with window focus once
await testObject.triggerSync(['windowFocus'], true);
// Make sure only one request is made
assert.deepEqual(target.requests, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
// Filter out machine requests
const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`));
// Make sure only one manifest request is made
assert.deepEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
});
test('test auto sync with non sync resource change does not trigger continuous syncs', async () => {
......@@ -99,8 +108,11 @@ suite('UserDataAutoSyncService', () => {
await testObject.triggerSync(['windowFocus'], true);
}
// Make sure only one request is made
assert.deepEqual(target.requests, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
// Filter out machine requests
const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`));
// Make sure only one manifest request is made
assert.deepEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
});
......
......@@ -50,8 +50,6 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } },
]);
});
......@@ -83,8 +81,6 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } },
]);
});
......@@ -194,7 +190,6 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '1' } },
]);
});
......@@ -239,7 +234,6 @@ suite('UserDataSyncService', () => {
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '1' } },
]);
});
......@@ -379,8 +373,6 @@ suite('UserDataSyncService', () => {
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '1' } },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
......@@ -397,8 +389,6 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } },
]);
});
......
......@@ -13,6 +13,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { IProductService } from 'vs/platform/product/common/productService';
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
......@@ -25,10 +27,12 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
@IInstantiationService instantiationService: IInstantiationService,
@IHostService hostService: IHostService,
@ITelemetryService telemetryService: ITelemetryService,
@IUserDataSyncMachinesService userDataSyncMachinesService: IUserDataSyncMachinesService,
@IProductService productService: IProductService,
@IStorageService storageService: IStorageService,
@IEnvironmentService environmentService: IEnvironmentService,
) {
super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, storageService, environmentService);
super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, productService, storageService, environmentService);
this._register(Event.debounce<string, string[]>(Event.any<string>(
Event.map(hostService.onDidChangeFocus, () => 'windowFocus'),
......
......@@ -26,6 +26,7 @@ class UserDataSyncReportIssueContribution extends Disposable implements IWorkben
private onAutoSyncError(error: UserDataSyncError): void {
switch (error.code) {
case UserDataSyncErrorCode.LocalTooManyRequests:
case UserDataSyncErrorCode.TooManyRequests:
this.notificationService.notify({
severity: Severity.Error,
message: localize('too many requests', "Turned off syncing preferences on this device because it is making too many requests."),
......
......@@ -39,8 +39,8 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
return this.channel.call('turnOn', [pullFirst]);
}
turnOff(): Promise<void> {
return this.channel.call('turnOff');
turnOff(everywhere: boolean): Promise<void> {
return this.channel.call('turnOff', [everywhere]);
}
}
......@@ -47,6 +47,7 @@ class UserDataSyncReportIssueContribution extends Disposable implements IWorkben
private onAutoSyncError(error: UserDataSyncError): void {
switch (error.code) {
case UserDataSyncErrorCode.LocalTooManyRequests:
case UserDataSyncErrorCode.TooManyRequests:
this.notificationService.notify({
severity: Severity.Error,
message: localize('too many requests', "Turned off syncing preferences on this device because it is making too many requests. Please report an issue by providing the sync logs."),
......
......@@ -224,15 +224,8 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
this.notificationService.info(localize('sync turned on', "Preferences sync is turned on"));
}
async turnoff(everywhere: boolean): Promise<void> {
await this.userDataAutoSyncService.turnOff();
if (everywhere) {
this.telemetryService.publicLog2('sync/turnOffEveryWhere');
await this.userDataSyncService.reset();
} else {
await this.userDataSyncService.resetLocal();
}
turnoff(everywhere: boolean): Promise<void> {
return this.userDataAutoSyncService.turnOff(everywhere);
}
private async handleFirstTimeSync(): Promise<boolean> {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册