diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 34a492b4443..d317071db46 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -53,7 +53,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient, UserDataSyncMachinesServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; @@ -70,6 +70,7 @@ import { AuthenticationTokenServiceChannel } from 'vs/platform/authentication/co import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService'; +import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -200,6 +201,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); + services.set(IUserDataSyncMachinesService, new SyncDescriptor(UserDataSyncMachinesService)); services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService)); services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); @@ -225,6 +227,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService); server.registerChannel('extensionTipsService', extensionTipsChannel); + const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService); + const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService); + server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); + const authTokenService = accessor.get(IAuthenticationTokenService); const authTokenChannel = new AuthenticationTokenServiceChannel(authTokenService); server.registerChannel('authToken', authTokenChannel); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 6b05b61848c..df47c2a26bf 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -159,16 +159,17 @@ export interface IResourceRefHandle { } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); +export type ServerResource = SyncResource | 'machines'; export interface IUserDataSyncStoreService { _serviceBrand: undefined; readonly userDataSyncStore: IUserDataSyncStore | undefined; - read(resource: SyncResource, oldValue: IUserData | null): Promise; - write(resource: SyncResource, content: string, ref: string | null): Promise; + read(resource: ServerResource, oldValue: IUserData | null): Promise; + write(resource: ServerResource, content: string, ref: string | null): Promise; manifest(): Promise; clear(): Promise; - getAllRefs(resource: SyncResource): Promise; - resolveContent(resource: SyncResource, ref: string): Promise; - delete(resource: SyncResource): Promise; + getAllRefs(resource: ServerResource): Promise; + resolveContent(resource: ServerResource, ref: string): Promise; + delete(resource: ServerResource): Promise; } export const IUserDataSyncBackupStoreService = createDecorator('IUserDataSyncBackupStoreService'); @@ -225,7 +226,11 @@ export class UserDataSyncError extends Error { } -export class UserDataSyncStoreError extends UserDataSyncError { } +export class UserDataSyncStoreError extends UserDataSyncError { + constructor(message: string, code: UserDataSyncErrorCode) { + super(message, code); + } +} //#endregion diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index cf73565adfa..e40608949a9 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -12,6 +12,7 @@ import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; +import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; export class UserDataSyncChannel implements IServerChannel { @@ -163,3 +164,23 @@ export class StorageKeysSyncRegistryChannelClient extends Disposable implements } } + +export class UserDataSyncMachinesServiceChannel implements IServerChannel { + + constructor(private readonly service: IUserDataSyncMachinesService) { } + + listen(_: unknown, event: string): Event { + throw new Error(`Event not found: ${event}`); + } + + async call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'getMachines': return this.service.getMachines(); + case 'updateName': return this.service.updateName(args[0]); + case 'unset': return this.service.unset(); + case 'disable': return this.service.disable(args[0]); + } + throw new Error('Invalid call'); + } + +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncMachines.ts b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts new file mode 100644 index 00000000000..4d6a539f2fa --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IUserDataSyncStoreService, IUserData, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { localize } from 'vs/nls'; +import { IProductService } from 'vs/platform/product/common/productService'; + +interface IMachineData { + id: string; + name: string; + disabled?: boolean; +} + +interface IMachinesData { + version: number; + machines: IMachineData[]; +} + +export type IUserDataSyncMachine = Readonly & { readonly isCurrent: boolean }; + + +export const IUserDataSyncMachinesService = createDecorator('IUserDataSyncMachinesService'); +export interface IUserDataSyncMachinesService { + _serviceBrand: any; + + getMachines(): Promise; + updateName(name: string): Promise; + unset(): Promise; + + disable(id: string): Promise +} + +export class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService { + + private static readonly VERSION = 1; + private static readonly RESOURCE = 'machines'; + + _serviceBrand: any; + + private readonly currentMachineIdPromise: Promise; + private userData: IUserData | null = null; + + constructor( + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService fileService: IFileService, + @IStorageService storageService: IStorageService, + @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + @IProductService private readonly productService: IProductService, + ) { + super(); + this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService); + } + + async getMachines(): Promise { + const currentMachineId = await this.currentMachineIdPromise; + const machineData = await this.readMachinesData(); + return machineData.machines.map(machine => ({ ...machine, ...{ isCurrent: machine.id === currentMachineId } })); + } + + async updateName(name: string): Promise { + const currentMachineId = await this.currentMachineIdPromise; + const machineData = await this.readMachinesData(); + let currentMachine = machineData.machines.find(({ id }) => id === currentMachineId); + if (currentMachine) { + currentMachine.name = name; + } else { + machineData.machines.push({ id: currentMachineId, name }); + } + await this.writeMachinesData(machineData); + } + + async unset(): Promise { + const currentMachineId = await this.currentMachineIdPromise; + const machineData = await this.readMachinesData(); + const updatedMachines = machineData.machines.filter(({ id }) => id !== currentMachineId); + if (updatedMachines.length !== machineData.machines.length) { + machineData.machines = updatedMachines; + await this.writeMachinesData(machineData); + } + } + + async disable(machineId: string): Promise { + const machineData = await this.readMachinesData(); + const machine = machineData.machines.find(({ id }) => id === machineId); + if (machine) { + machine.disabled = true; + await this.writeMachinesData(machineData); + } + } + + private async readMachinesData(): Promise { + this.userData = await this.userDataSyncStoreService.read(UserDataSyncMachinesService.RESOURCE, this.userData); + const machinesData = this.parse(this.userData); + if (machinesData.version !== UserDataSyncMachinesService.VERSION) { + throw new Error(localize('error incompatible', "Cannot read machines data as the current version is incompatible. Please update {0} and try again.", this.productService.nameLong)); + } + return machinesData; + } + + private async writeMachinesData(machinesData: IMachinesData): Promise { + const content = JSON.stringify(machinesData); + const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null); + this.userData = { ref, content }; + } + + private parse(userData: IUserData): IMachinesData { + if (userData.content !== null) { + try { + return JSON.parse(userData.content); + } catch (e) { + this.logService.error(e); + } + } + return { + version: UserDataSyncMachinesService.VERSION, + machines: [] + }; + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 845645d9ba3..8be5fa2d62f 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -20,6 +20,9 @@ import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSy import { isEqual } from 'vs/base/common/resources'; import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync'; import { Throttler } from 'vs/base/common/async'; +import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { isWeb } from 'vs/base/common/platform'; type SyncClassification = { source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -67,7 +70,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, + @IProductService private readonly productService: IProductService ) { super(); this.syncThrottler = new Throttler(); @@ -131,7 +136,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ // Server has no data but this machine was synced before if (manifest === null && await this.hasPreviouslySynced()) { - // Sync was turned off from other machine + // Sync was turned off in the cloud throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); } @@ -141,6 +146,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ throw new UserDataSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired); } + const machines = await this.userDataSyncMachinesService.getMachines(); + 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.unset(); + // 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); + } + for (const synchroniser of this.synchronisers) { try { await synchroniser.sync(manifest); @@ -160,6 +176,20 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL); } + if (!currentMachine) { + // add current machine to sync server + const namePrefix = `${this.productService.nameLong}${isWeb ? ' (Web)' : ''}`; + const nameRegEx = new RegExp(`${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; + } + await this.userDataSyncMachinesService.updateName(`${namePrefix} #${nameIndex + 1}`); + } + this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`); this.updateLastSyncTime(); @@ -247,14 +277,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ async reset(): Promise { await this.checkEnablement(); - await this.resetRemote(); await this.resetLocal(); + await this.resetRemote(); } async resetLocal(): Promise { await this.checkEnablement(); this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL); this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL); + await this.userDataSyncMachinesService.unset(); for (const synchroniser of this.synchronisers) { try { await synchroniser.resetLocal(); diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 252d31728a9..f1f70bad4b9 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, SyncResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -64,7 +64,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService, telemetryService); } - async getAllRefs(resource: SyncResource): Promise { + async getAllRefs(resource: ServerResource): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -72,17 +72,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const uri = joinPath(this.userDataSyncStore.url, 'resource', resource); const headers: IHeaders = {}; - const context = await this.request({ type: 'GET', url: uri.toString(), headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'GET', url: uri.toString(), headers }, CancellationToken.None); if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } const result = await asJson<{ url: string, created: number }[]>(context) || []; return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ })); } - async resolveContent(resource: SyncResource, ref: string): Promise { + async resolveContent(resource: ServerResource, ref: string): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -91,17 +91,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const headers: IHeaders = {}; headers['Cache-Control'] = 'no-cache'; - const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None); if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } const content = await asText(context); return content; } - async delete(resource: SyncResource): Promise { + async delete(resource: ServerResource): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -109,14 +109,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString(); const headers: IHeaders = {}; - const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None); if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } } - async read(resource: SyncResource, oldValue: IUserData | null): Promise { + async read(resource: ServerResource, oldValue: IUserData | null): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -129,7 +129,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn headers['If-None-Match'] = oldValue.ref; } - const context = await this.request({ type: 'GET', url, headers }, resource, CancellationToken.None); + const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None); if (context.res.statusCode === 304) { // There is no new value. Hence return the old value. @@ -137,18 +137,18 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } const ref = context.res.headers['etag']; if (!ref) { - throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource); + throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef); } const content = await asText(context); return { ref, content }; } - async write(resource: SyncResource, data: string, ref: string | null): Promise { + async write(resource: ServerResource, data: string, ref: string | null): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -159,15 +159,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn headers['If-Match'] = ref; } - const context = await this.request({ type: 'POST', url, data, headers }, resource, CancellationToken.None); + const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None); if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } const newRef = context.res.headers['etag']; if (!newRef) { - throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource); + throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef); } return newRef; } @@ -180,7 +180,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const url = joinPath(this.userDataSyncStore.url, 'manifest').toString(); const headers: IHeaders = { 'Content-Type': 'application/json' }; - const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None); if (!isSuccess(context)) { throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } @@ -214,7 +214,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const url = joinPath(this.userDataSyncStore.url, 'resource').toString(); const headers: IHeaders = { 'Content-Type': 'text/plain' }; - const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None); if (!isSuccess(context)) { throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); @@ -229,10 +229,10 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL); } - private async request(options: IRequestOptions, source: SyncResource | undefined, token: CancellationToken): Promise { + private async request(options: IRequestOptions, token: CancellationToken): Promise { const authToken = this.authTokenService.token; if (!authToken) { - throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, source); + throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized); } const commonHeaders = await this.commonHeadersPromise; @@ -252,30 +252,30 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn this.logService.trace('Request finished', { url: options.url, status: context.res.statusCode }); } catch (e) { if (!(e instanceof UserDataSyncStoreError)) { - e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, source); + e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused); } throw e; } if (context.res.statusCode === 401) { this.authTokenService.sendTokenFailed(); - throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source); + throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized); } if (context.res.statusCode === 403) { - throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' is Forbidden (403).`, UserDataSyncErrorCode.Forbidden, source); + throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' is Forbidden (403).`, UserDataSyncErrorCode.Forbidden); } if (context.res.statusCode === 412) { - throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.RemotePreconditionFailed, source); + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.RemotePreconditionFailed); } if (context.res.statusCode === 413) { - throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge, source); + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge); } if (context.res.statusCode === 429) { - throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests, source); + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests); } return context; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index ac7b9a146d7..6c111d035e6 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -32,6 +32,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IAction, Action } from 'vs/base/common/actions'; import { IUserDataSyncWorkbenchService } from 'vs/workbench/services/userDataSync/common/userDataSyncWorkbenchService'; +import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export class UserDataSyncViewPaneContainer extends ViewPaneContainer { @@ -69,19 +71,21 @@ export class UserDataSyncDataViews extends Disposable { @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, ) { super(); this.registerViews(container); } private registerViews(container: ViewContainer): void { - const remoteView = this.registerView(container, true, true); + const remoteView = this.registerDataView(container, true, true); this.registerRemoteViewActions(remoteView); - this.registerView(container, false, false); + this.registerDataView(container, false, false); + this.registerMachinesView(container); } - private registerView(container: ViewContainer, remote: boolean, showByDefault: boolean): TreeView { + private registerDataView(container: ViewContainer, remote: boolean, showByDefault: boolean): TreeView { const id = `workbench.views.sync.${remote ? 'remote' : 'local'}DataView`; const showByDefaultContext = new RawContextKey(id, showByDefault); const viewEnablementContext = showByDefaultContext.bindTo(this.contextKeyService); @@ -148,11 +152,69 @@ export class UserDataSyncDataViews extends Disposable { } }); - this.registerActions(id); + this.registerDataViewActions(id); return treeView; } - private registerActions(viewId: string) { + private registerMachinesView(container: ViewContainer): void { + const that = this; + const id = `workbench.views.sync.machines`; + const name = localize('synced machines', "Synced Machines"); + const treeView = this.instantiationService.createInstance(TreeView, id, name); + treeView.showRefreshAction = true; + const disposable = treeView.onDidChangeVisibility(visible => { + if (visible && !treeView.dataProvider) { + disposable.dispose(); + treeView.dataProvider = new UserDataSyncMachinesViewDataProvider(treeView, this.userDataSyncMachinesService); + } + }); + const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + viewsRegistry.registerViews([{ + id, + name, + ctorDescriptor: new SyncDescriptor(TreeViewPane), + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS), + canToggleVisibility: true, + canMoveView: true, + treeView, + collapsed: false, + order: 200, + }], container); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.sync.editCurrentMachineName`, + title: localize('workbench.actions.sync.editCurrentMachineName', "Edit Name"), + icon: Codicon.edit, + menu: { + id: MenuId.ViewItemContext, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', id), ContextKeyEqualsExpr.create('viewItem', 'current-machine')), + group: 'inline', + }, + }); + } + async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { + const quickInputService = accessor.get(IQuickInputService); + const inputBox = quickInputService.createInputBox(); + inputBox.placeholder = localize('placeholder', "Enter the name of the machine"); + inputBox.show(); + return new Promise((c, e) => { + inputBox.onDidAccept(async () => { + const name = inputBox.value; + if (name) { + await that.userDataSyncMachinesService.updateName(name); + await treeView.refresh(); + } + inputBox.dispose(); + c(); + }); + }); + } + }); + } + + private registerDataViewActions(viewId: string) { registerAction2(class extends Action2 { constructor() { super({ @@ -317,6 +379,28 @@ class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider { } } +class UserDataSyncMachinesViewDataProvider implements ITreeViewDataProvider { + + constructor( + private readonly treeView: TreeView, + private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, + ) { } + + async getChildren(): Promise { + let machines = await this.userDataSyncMachinesService.getMachines(); + machines = machines.filter(m => !m.disabled).sort((m1, m2) => m1.isCurrent ? -1 : 1); + this.treeView.message = machines.length ? undefined : localize('no machines', "No Machines"); + return machines.map(({ id, name, isCurrent }) => ({ + handle: id, + collapsibleState: TreeItemCollapsibleState.None, + label: { label: name }, + description: isCurrent ? localize({ key: 'current', comment: ['Current machine'] }, "Current") : undefined, + themeIcon: Codicon.vm, + contextValue: isCurrent ? 'current-machine' : 'other-machine' + })); + } +} + function label(date: Date): string { return date.toLocaleDateString() + ' ' + pad(date.getHours(), 2) + diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService.ts new file mode 100644 index 00000000000..8a133d223c2 --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines'; + +class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService { + + _serviceBrand: undefined; + + private readonly channel: IChannel; + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService + ) { + super(); + this.channel = sharedProcessService.getChannel('userDataSyncMachines'); + } + + getMachines(): Promise { + return this.channel.call('getMachines'); + } + + updateName(name: string): Promise { + return this.channel.call('updateName', [name]); + } + + unset(): Promise { + return this.channel.call('unset'); + } + + disable(id: string): Promise { + return this.channel.call('disable', [id]); + } + +} + +registerSingleton(IUserDataSyncMachinesService, UserDataSyncMachinesService); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index b68c4dce757..1f90204b3e3 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -51,6 +51,7 @@ import 'vs/workbench/services/url/electron-browser/urlService'; import 'vs/workbench/services/workspaces/electron-browser/workspacesService'; import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService'; import 'vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; import 'vs/workbench/services/authentication/electron-browser/authenticationTokenService'; import 'vs/workbench/services/authentication/browser/authenticationService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index fb31b7ca292..9d89286a67e 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -64,6 +64,7 @@ import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { TunnelService } from 'vs/workbench/services/remote/common/tunnelService'; import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; +import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { StorageKeysSyncRegistryService, IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { AuthenticationService, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; @@ -86,6 +87,7 @@ registerSingleton(ILoggerService, FileLoggerService); registerSingleton(IAuthenticationService, AuthenticationService); registerSingleton(IUserDataSyncLogService, UserDataSyncLogService); registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); +registerSingleton(IUserDataSyncMachinesService, UserDataSyncMachinesService); registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); registerSingleton(IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService); registerSingleton(IAuthenticationTokenService, AuthenticationTokenService);