mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-09 16:24:45 +01:00
Fix #100220
This commit is contained in:
@@ -27,6 +27,7 @@ import { equals } from 'vs/base/common/arrays';
|
||||
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
|
||||
type SyncSourceClassification = {
|
||||
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
@@ -75,6 +76,8 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
protected readonly lastSyncResource: URI;
|
||||
protected readonly syncResourceLogLabel: string;
|
||||
|
||||
private syncHeaders: IHeaders = {};
|
||||
|
||||
constructor(
|
||||
readonly resource: SyncResource,
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@@ -150,39 +153,44 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
async sync(manifest: IUserDataManifest | null): Promise<void> {
|
||||
if (!this.isEnabled()) {
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
await this.stop();
|
||||
}
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
|
||||
return;
|
||||
}
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`);
|
||||
return;
|
||||
}
|
||||
if (this.status === SyncStatus.Syncing) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
|
||||
|
||||
let status: SyncStatus = SyncStatus.Idle;
|
||||
async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<void> {
|
||||
try {
|
||||
status = await this.performSync(remoteUserData, lastSyncUserData);
|
||||
if (status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`);
|
||||
} else if (status === SyncStatus.Idle) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`);
|
||||
this.syncHeaders = { ...headers };
|
||||
if (!this.isEnabled()) {
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
await this.stop();
|
||||
}
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
|
||||
return;
|
||||
}
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`);
|
||||
return;
|
||||
}
|
||||
if (this.status === SyncStatus.Syncing) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
|
||||
|
||||
let status: SyncStatus = SyncStatus.Idle;
|
||||
try {
|
||||
status = await this.performSync(remoteUserData, lastSyncUserData);
|
||||
if (status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`);
|
||||
} else if (status === SyncStatus.Idle) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`);
|
||||
}
|
||||
} finally {
|
||||
this.setStatus(status);
|
||||
}
|
||||
} finally {
|
||||
this.setStatus(status);
|
||||
this.syncHeaders = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,14 +454,14 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return { ref: refOrLastSyncData, content };
|
||||
} else {
|
||||
const lastSyncUserData: IUserData | null = refOrLastSyncData ? { ref: refOrLastSyncData.ref, content: refOrLastSyncData.syncData ? JSON.stringify(refOrLastSyncData.syncData) : null } : null;
|
||||
return this.userDataSyncStoreService.read(this.resource, lastSyncUserData);
|
||||
return this.userDataSyncStoreService.read(this.resource, lastSyncUserData, this.syncHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateRemoteUserData(content: string, ref: string | null): Promise<IRemoteUserData> {
|
||||
const machineId = await this.currentMachineIdPromise;
|
||||
const syncData: ISyncData = { version: this.version, machineId, content };
|
||||
ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref);
|
||||
ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref, this.syncHeaders);
|
||||
return { ref, syncData };
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
|
||||
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { isArray, isString, isObject } from 'vs/base/common/types';
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
|
||||
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
|
||||
|
||||
@@ -162,16 +163,20 @@ export type ServerResource = SyncResource | 'machines';
|
||||
export interface IUserDataSyncStoreService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly userDataSyncStore: IUserDataSyncStore | undefined;
|
||||
|
||||
readonly onTokenFailed: Event<void>;
|
||||
readonly onTokenSucceed: Event<void>;
|
||||
setAuthToken(token: string, type: string): void;
|
||||
read(resource: ServerResource, oldValue: IUserData | null): Promise<IUserData>;
|
||||
write(resource: ServerResource, content: string, ref: string | null): Promise<string>;
|
||||
manifest(): Promise<IUserDataManifest | null>;
|
||||
|
||||
// Sync requests
|
||||
manifest(headers?: IHeaders): Promise<IUserDataManifest | null>;
|
||||
read(resource: ServerResource, oldValue: IUserData | null, headers?: IHeaders): Promise<IUserData>;
|
||||
write(resource: ServerResource, content: string, ref: string | null, headers?: IHeaders): Promise<string>;
|
||||
clear(): Promise<void>;
|
||||
delete(resource: ServerResource): Promise<void>;
|
||||
|
||||
getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]>;
|
||||
resolveContent(resource: ServerResource, ref: string): Promise<string | null>;
|
||||
delete(resource: ServerResource): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncBackupStoreService = createDecorator<IUserDataSyncBackupStoreService>('IUserDataSyncBackupStoreService');
|
||||
@@ -301,7 +306,7 @@ export interface IUserDataSynchroniser {
|
||||
|
||||
pull(): Promise<void>;
|
||||
push(): Promise<void>;
|
||||
sync(manifest: IUserDataManifest | null): Promise<void>;
|
||||
sync(manifest: IUserDataManifest | null, headers?: IHeaders): Promise<void>;
|
||||
replace(uri: URI): Promise<boolean>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ 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 { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
|
||||
type SyncClassification = {
|
||||
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
@@ -159,7 +161,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
|
||||
this.telemetryService.publicLog2('sync/getmanifest');
|
||||
let manifest = await this.userDataSyncStoreService.manifest();
|
||||
const syncHeaders: IHeaders = { 'X-Execution-Id': generateUuid() };
|
||||
let manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
|
||||
|
||||
// Server has no data but this machine was synced before
|
||||
if (manifest === null && await this.hasPreviouslySynced()) {
|
||||
@@ -195,7 +198,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await synchroniser.sync(manifest);
|
||||
await synchroniser.sync(manifest, syncHeaders);
|
||||
} catch (e) {
|
||||
this.handleSynchronizerError(e, synchroniser.resource);
|
||||
this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]);
|
||||
@@ -204,7 +207,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
// After syncing, get the manifest if it was not available before
|
||||
if (manifest === null) {
|
||||
manifest = await this.userDataSyncStoreService.manifest();
|
||||
manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
|
||||
}
|
||||
|
||||
// Return if cancellation is requested
|
||||
|
||||
@@ -124,13 +124,13 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
}
|
||||
|
||||
async read(resource: ServerResource, oldValue: IUserData | null): Promise<IUserData> {
|
||||
async read(resource: ServerResource, oldValue: IUserData | null, headers: IHeaders = {}): Promise<IUserData> {
|
||||
if (!this.userDataSyncStore) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, 'latest').toString();
|
||||
const headers: IHeaders = {};
|
||||
headers = { ...headers };
|
||||
// Disable caching as they are cached by synchronisers
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
if (oldValue) {
|
||||
@@ -156,13 +156,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
return { ref, content };
|
||||
}
|
||||
|
||||
async write(resource: ServerResource, data: string, ref: string | null): Promise<string> {
|
||||
async write(resource: ServerResource, data: string, ref: string | null, headers: IHeaders = {}): Promise<string> {
|
||||
if (!this.userDataSyncStore) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'text/plain' };
|
||||
headers = { ...headers };
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
if (ref) {
|
||||
headers['If-Match'] = ref;
|
||||
}
|
||||
@@ -180,13 +181,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
return newRef;
|
||||
}
|
||||
|
||||
async manifest(): Promise<IUserDataManifest | null> {
|
||||
async manifest(headers: IHeaders = {}): Promise<IUserDataManifest | null> {
|
||||
if (!this.userDataSyncStore) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'manifest').toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'application/json' };
|
||||
headers = { ...headers };
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
|
||||
if (!isSuccess(context)) {
|
||||
|
||||
@@ -583,4 +583,24 @@ suite('UserDataSyncService', () => {
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('test sync send execution id header', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
await testObject.sync();
|
||||
|
||||
for (const request of target.requestsWithAllHeaders) {
|
||||
const hasExecutionIdHeader = request.headers && request.headers['X-Execution-Id'] && request.headers['X-Execution-Id'].length > 0;
|
||||
if (request.url.startsWith(`${target.url}/v1/resource/machines`)) {
|
||||
assert.ok(!hasExecutionIdHeader, `Should not have execution header: ${request.url}`);
|
||||
} else {
|
||||
assert.ok(hasExecutionIdHeader, `Should have execution header: ${request.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user