diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 56f7ed460f3..68b3606086f 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -26,7 +26,7 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProper import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWindowsService, ActiveWindowManager } from 'vs/platform/windows/common/windows'; import { WindowsService } from 'vs/platform/windows/electron-browser/windowsService'; import { ipcRenderer } from 'electron'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; @@ -51,6 +51,11 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IUserDataSyncService, IUserDataSyncStoreService, ISettingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncService, UserDataAutoSync } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { SettingsMergeChannelClient } from 'vs/platform/userDataSync/common/settingsSyncIpc'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -117,6 +122,11 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const windowsService = new WindowsService(mainProcessService); services.set(IWindowsService, windowsService); + const activeWindowManager = new ActiveWindowManager(windowsService); + const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); + const settingsMergeChannel = server.getChannel('settingsMerge', activeWindowRouter); + services.set(ISettingsMergeService, new SettingsMergeChannelClient(settingsMergeChannel)); + // Files const fileService = new FileService(logService); services.set(IFileService, fileService); @@ -163,6 +173,8 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); + services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); + services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); const instantiationService2 = instantiationService.createChild(services); @@ -180,6 +192,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const diagnosticsChannel = new DiagnosticsChannel(diagnosticsService); server.registerChannel('diagnostics', diagnosticsChannel); + const userDataSyncService = accessor.get(IUserDataSyncService); + const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService); + server.registerChannel('userDataSync', userDataSyncChannel); + // clean up deprecated extensions (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); // update localizations cache @@ -189,7 +205,8 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat instantiationService2.createInstance(NodeCachedDataCleaner), instantiationService2.createInstance(LanguagePackCachedDataCleaner), instantiationService2.createInstance(StorageDataCleaner), - instantiationService2.createInstance(LogsDataCleaner) + instantiationService2.createInstance(LogsDataCleaner), + instantiationService2.createInstance(UserDataAutoSync) )); disposables.add(extensionManagementService as ExtensionManagementService); }); diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 31cca78bd31..fdd4e9997a0 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -114,6 +114,7 @@ export interface IEnvironmentService { // user roaming data userRoamingDataHome: URI; settingsResource: URI; + settingsSyncPreviewResource: URI; keybindingsResource: URI; keyboardLayoutResource: URI; localeResource: URI; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 36f275fbaf5..0df72e178ad 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -116,6 +116,9 @@ export class EnvironmentService implements IEnvironmentService { @memoize get settingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'settings.json'); } + @memoize + get settingsSyncPreviewResource(): URI { return resources.joinPath(this.userRoamingDataHome, '.settings.json'); } + @memoize get machineSettingsHome(): URI { return URI.file(path.join(this.userDataPath, 'Machine')); } diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 9ebf057bd4f..fb001ed8ef0 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -6,7 +6,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, ISettingsMergeService, IUserDataSyncStoreService, SETTINGS_CONFLICTS_RESOURCE } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, ISettingsMergeService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse, ParseError } from 'vs/base/common/json'; import { localize } from 'vs/nls'; @@ -14,7 +14,6 @@ import { Emitter, Event } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs/base/common/async'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { URI } from 'vs/base/common/uri'; interface ISyncPreviewResult { readonly fileContent: IFileContent | null; @@ -40,8 +39,6 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { private _onDidChangeLocal: Emitter = this._register(new Emitter()); readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; - readonly conflicts: URI = SETTINGS_CONFLICTS_RESOURCE; - constructor( @IFileService private readonly fileService: IFileService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @@ -77,7 +74,11 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } } - async sync(): Promise { + async sync(_continue?: boolean): Promise { + + if (_continue) { + return this.continueSync(); + } if (this.status !== SyncStatus.Idle) { return false; @@ -110,7 +111,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } } - async continueSync(): Promise { + private async continueSync(): Promise { if (this.status !== SyncStatus.HasConflicts) { return false; } @@ -123,8 +124,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { return; } - if (await this.fileService.exists(this.conflicts)) { - const settingsPreivew = await this.fileService.readFile(this.conflicts); + if (await this.fileService.exists(this.environmentService.settingsSyncPreviewResource)) { + const settingsPreivew = await this.fileService.readFile(this.environmentService.settingsSyncPreviewResource); const content = settingsPreivew.value.toString(); if (this.hasErrors(content)) { return Promise.reject(localize('errorInvalidSettings', "Unable to sync settings. Please resolve conflicts without any errors/warnings and try again.")); @@ -143,7 +144,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } // Delete the preview - await this.fileService.del(this.conflicts); + await this.fileService.del(this.environmentService.settingsSyncPreviewResource); } this.syncPreviewResultPromise = null; @@ -175,7 +176,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { if (fileContent && !remoteUserData) { this.logService.trace('Settings Sync: Remote contents does not exist. So sync with settings file.'); hasRemoteChanged = true; - await this.fileService.writeFile(this.conflicts, VSBuffer.fromString(fileContent.value.toString())); + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(fileContent.value.toString())); return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; } @@ -183,7 +184,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { if (remoteUserData && !fileContent) { this.logService.trace('Settings Sync: Settings file does not exist. So sync with remote contents'); hasLocalChanged = true; - await this.fileService.writeFile(this.conflicts, VSBuffer.fromString(remoteUserData.content)); + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(remoteUserData.content)); return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; } @@ -202,7 +203,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { if (hasLocalChanged || hasRemoteChanged) { // Sync only if there are changes hasConflicts = this.hasErrors(mergeContent); - await this.fileService.writeFile(this.conflicts, VSBuffer.fromString(mergeContent)); + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(mergeContent)); return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; } } diff --git a/src/vs/platform/userDataSync/common/settingsSyncIpc.ts b/src/vs/platform/userDataSync/common/settingsSyncIpc.ts new file mode 100644 index 00000000000..a8e4cac6ac5 --- /dev/null +++ b/src/vs/platform/userDataSync/common/settingsSyncIpc.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Event } from 'vs/base/common/event'; +import { ISettingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; + +export class SettingsMergeChannel implements IServerChannel { + + constructor(private readonly service: ISettingsMergeService) { } + + listen(_: unknown, event: string): Event { + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'merge': return this.service.merge(args[0], args[1], args[2]); + } + throw new Error('Invalid call'); + } +} + +export class SettingsMergeChannelClient implements ISettingsMergeService { + + _serviceBrand: undefined; + + constructor(private readonly channel: IChannel) { + } + + merge(localContent: string, remoteContent: string, baseContent: string | null): Promise { + return this.channel.call('merge', [localContent, remoteContent, baseContent]); + } + +} diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index f54e3f4e62f..a6bf31b22ac 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -5,7 +5,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; -import { URI } from 'vs/base/common/uri'; export interface IUserData { ref: string; @@ -41,6 +40,10 @@ export interface IUserDataSyncStoreService { write(key: string, content: string, ref: string | null): Promise; } +export enum SyncSource { + Settings = 1, + Extensions +} export enum SyncStatus { Uninitialized = 'uninitialized', @@ -49,24 +52,20 @@ export enum SyncStatus { HasConflicts = 'hasConflicts', } -export const USER_DATA_PREVIEW_SCHEME = 'vscode-userdata-preview'; -export const SETTINGS_CONFLICTS_RESOURCE = URI.file('Settings-Preview').with({ scheme: USER_DATA_PREVIEW_SCHEME }); - export interface ISynchroniser { - readonly conflicts: URI | null; readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly onDidChangeLocal: Event; - sync(): Promise; - continueSync(): Promise; + sync(_continue?: boolean): Promise; } export const IUserDataSyncService = createDecorator('IUserDataSyncService'); export interface IUserDataSyncService extends ISynchroniser { _serviceBrand: any; + readonly conflictsSource: SyncSource | null; } export const ISettingsMergeService = createDecorator('ISettingsMergeService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts new file mode 100644 index 00000000000..20b40039141 --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Event } from 'vs/base/common/event'; +import { IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync'; + +export class UserDataSyncChannel implements IServerChannel { + + constructor(private readonly service: IUserDataSyncService) { } + + listen(_: unknown, event: string): Event { + switch (event) { + case 'onDidChangeStatus': return this.service.onDidChangeStatus; + case 'onDidChangeLocal': return this.service.onDidChangeLocal; + } + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'sync': return this.service.sync(args[0]); + case 'getConflictsSource': return Promise.resolve(this.service.conflictsSource); + } + throw new Error('Invalid call'); + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 471a2e644d7..17988d1184a 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { Emitter, Event } from 'vs/base/common/event'; -import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { timeout } from 'vs/base/common/async'; export class UserDataSyncService extends Disposable implements IUserDataSyncService { @@ -18,11 +19,14 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _status: SyncStatus = SyncStatus.Uninitialized; get status(): SyncStatus { return this._status; } - private _onDidChangStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeStatus: Event = this._onDidChangStatus.event; + private _onDidChangeStatus: Emitter = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; readonly onDidChangeLocal: Event; + private _conflictsSource: SyncSource | null = null; + get conflictsSource(): SyncSource | null { return this._conflictsSource; } + constructor( @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -36,43 +40,27 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal)); } - get conflicts(): URI | null { - const synchroniser = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0]; - return synchroniser ? synchroniser.conflicts : null; - } - - async sync(): Promise { + async sync(_continue?: boolean): Promise { if (!this.userDataSyncStoreService.enabled) { throw new Error('Not enabled'); } for (const synchroniser of this.synchronisers) { - if (!await synchroniser.sync()) { + if (!await synchroniser.sync(_continue)) { return false; } } return true; } - async continueSync(): Promise { - if (!this.userDataSyncStoreService.enabled) { - throw new Error('Not enabled'); - } - for (const synchroniser of this.synchronisers) { - if (await synchroniser.continueSync()) { - return true; - } - } - return false; - } - private updateStatus(): void { + this._conflictsSource = this.computeConflictsSource(); this.setStatus(this.computeStatus()); } private setStatus(status: SyncStatus): void { if (this._status !== status) { this._status = status; - this._onDidChangStatus.fire(status); + this._onDidChangeStatus.fire(status); } } @@ -89,4 +77,52 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return SyncStatus.Idle; } + private computeConflictsSource(): SyncSource | null { + const source = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0]; + if (source) { + if (source instanceof SettingsSynchroniser) { + return SyncSource.Settings; + } + } + return null; + } + +} + +export class UserDataAutoSync extends Disposable { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, + @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + ) { + super(); + if (userDataSyncStoreService.enabled) { + this.sync(true); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('userConfiguration.enableSync'))(() => this.sync(true))); + + // Sync immediately if there is a local change. + this._register(Event.debounce(this.userDataSyncService.onDidChangeLocal, () => undefined, 500)(() => this.sync(false))); + } + } + + private async sync(loop: boolean): Promise { + if (this.isSyncEnabled()) { + try { + await this.userDataSyncService.sync(); + } catch (e) { + // Ignore errors + } + if (loop) { + await timeout(1000 * 5); // Loop sync for every 5s. + this.sync(loop); + } + } + } + + private isSyncEnabled(): boolean { + const { user: userLocal } = this.configurationService.inspect('userConfiguration.enableSync'); + return userLocal === undefined || userLocal; + } + } diff --git a/src/vs/workbench/contrib/userData/browser/media/sync-push-dark.svg b/src/vs/workbench/contrib/userDataSync/browser/media/sync-push-dark.svg similarity index 100% rename from src/vs/workbench/contrib/userData/browser/media/sync-push-dark.svg rename to src/vs/workbench/contrib/userDataSync/browser/media/sync-push-dark.svg diff --git a/src/vs/workbench/contrib/userData/browser/media/sync-push-light.svg b/src/vs/workbench/contrib/userDataSync/browser/media/sync-push-light.svg similarity index 100% rename from src/vs/workbench/contrib/userData/browser/media/sync-push-light.svg rename to src/vs/workbench/contrib/userDataSync/browser/media/sync-push-light.svg diff --git a/src/vs/workbench/contrib/userData/browser/userData.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts similarity index 81% rename from src/vs/workbench/contrib/userData/browser/userData.contribution.ts rename to src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index ed95a7e2571..4edc5aa34e4 100644 --- a/src/vs/workbench/contrib/userData/browser/userData.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IUserDataSyncService, SyncStatus, USER_DATA_PREVIEW_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; import { localize } from 'vs/nls'; import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -16,7 +16,6 @@ import { MenuRegistry, MenuId, IMenuItem } from 'vs/platform/actions/common/acti import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; -import { timeout } from 'vs/base/common/async'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { URI } from 'vs/base/common/uri'; import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; @@ -25,8 +24,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { Event } from 'vs/base/common/event'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { IFileService } from 'vs/platform/files/common/files'; -import { InMemoryFileSystemProvider } from 'vs/workbench/services/userData/common/inMemoryUserDataProvider'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { isEqual } from 'vs/base/common/resources'; const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); @@ -46,39 +45,6 @@ Registry.as(ConfigurationExtensions.Configuration) } }); -class UserDataSyncContribution extends Disposable implements IWorkbenchContribution { - - constructor( - @IFileService fileService: IFileService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, - ) { - super(); - this._register(fileService.registerProvider(USER_DATA_PREVIEW_SCHEME, new InMemoryFileSystemProvider())); - this.sync(true); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('userConfiguration.enableSync') && this.configurationService.getValue('userConfiguration.enableSync')) - (() => this.sync(true))); - - // Sync immediately if there is a local change. - this._register(Event.debounce(this.userDataSyncService.onDidChangeLocal, () => undefined, 500)(() => this.sync(false))); - } - - private async sync(loop: boolean): Promise { - if (this.configurationService.getValue('userConfiguration.enableSync')) { - try { - await this.userDataSyncService.sync(); - } catch (e) { - // Ignore errors - } - if (loop) { - await timeout(1000 * 5); // Loop sync for every 5s. - this.sync(loop); - } - } - } - -} - const SYNC_PUSH_LIGHT_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userData/browser/media/sync-push-light.svg`)); const SYNC_PUSH_DARK_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userData/browser/media/sync-push-dark.svg`)); class SyncActionsContribution extends Disposable implements IWorkbenchContribution { @@ -96,6 +62,7 @@ class SyncActionsContribution extends Disposable implements IWorkbenchContributi @IEditorService private readonly editorService: IEditorService, @ITextFileService private readonly textFileService: ITextFileService, @IHistoryService private readonly historyService: IHistoryService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, ) { super(); this.syncEnablementContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); @@ -142,17 +109,14 @@ class SyncActionsContribution extends Disposable implements IWorkbenchContributi private async continueSync(): Promise { // Get the preview editor - const editorInput = this.editorService.editors.filter(input => { - const resource = input.getResource(); - return resource && resource.scheme === USER_DATA_PREVIEW_SCHEME; - })[0]; + const editorInput = this.editorService.editors.filter(input => isEqual(input.getResource(), this.workbenchEnvironmentService.settingsSyncPreviewResource))[0]; // Save the preview if (editorInput && editorInput.isDirty()) { await this.textFileService.save(editorInput.getResource()!); } try { // Continue Sync - await this.userDataSyncService.continueSync(); + await this.userDataSyncService.sync(true); } catch (error) { this.notificationService.error(error); return; @@ -164,10 +128,9 @@ class SyncActionsContribution extends Disposable implements IWorkbenchContributi } private async handleConflicts(): Promise { - const resource = this.userDataSyncService.conflicts; - if (resource) { + if (this.userDataSyncService.conflictsSource === SyncSource.Settings ) { const resourceInput = { - resource, + resource: this.workbenchEnvironmentService.settingsSyncPreviewResource, options: { preserveFocus: false, pinned: false, @@ -237,11 +200,10 @@ class SyncActionsContribution extends Disposable implements IWorkbenchContributi }, group: 'navigation', order: 1, - when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), ResourceContextKey.Scheme.isEqualTo(USER_DATA_PREVIEW_SCHEME)), + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), ResourceContextKey.Resource.isEqualTo(this.workbenchEnvironmentService.settingsSyncPreviewResource.toString())), }); } } const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(SyncActionsContribution, LifecyclePhase.Starting); -workbenchRegistry.registerWorkbenchContribution(UserDataSyncContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts new file mode 100644 index 00000000000..2b8fb68f933 --- /dev/null +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ISettingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { SettingsMergeChannel } from 'vs/platform/userDataSync/common/settingsSyncIpc'; + +class UserDataSyncServicesContribution implements IWorkbenchContribution { + + constructor( + @ISettingsMergeService settingsMergeService: ISettingsMergeService, + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + sharedProcessService.registerChannel('settingsMerge', new SettingsMergeChannel(settingsMergeService)); + } +} + +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(UserDataSyncServicesContribution, LifecyclePhase.Starting); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index a68e9cef955..6ccc6ffcbbf 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -84,6 +84,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment this.configuration.machineId = generateUuid(); this.userRoamingDataHome = URI.file('/User').with({ scheme: Schemas.userData }); this.settingsResource = joinPath(this.userRoamingDataHome, 'settings.json'); + this.settingsSyncPreviewResource = joinPath(this.userRoamingDataHome, '.settings.json'); this.keybindingsResource = joinPath(this.userRoamingDataHome, 'keybindings.json'); this.keyboardLayoutResource = joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); this.localeResource = joinPath(this.userRoamingDataHome, 'locale.json'); @@ -141,6 +142,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment appSettingsHome: URI; userRoamingDataHome: URI; settingsResource: URI; + settingsSyncPreviewResource: URI; keybindingsResource: URI; keyboardLayoutResource: URI; localeResource: URI; diff --git a/src/vs/workbench/services/userData/common/settingsMergeService.ts b/src/vs/workbench/services/userDataSync/common/settingsMergeService.ts similarity index 100% rename from src/vs/workbench/services/userData/common/settingsMergeService.ts rename to src/vs/workbench/services/userDataSync/common/settingsMergeService.ts diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts new file mode 100644 index 00000000000..7a1fe399ff1 --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncStatus, SyncSource, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class UserDataSyncService extends Disposable implements IUserDataSyncService { + + _serviceBrand: undefined; + + private readonly channel: IChannel; + + private _status: SyncStatus = SyncStatus.Uninitialized; + get status(): SyncStatus { return this._status; } + private _onDidChangeStatus: Emitter = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; + + get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } + + private _conflictsSource: SyncSource | null = null; + get conflictsSource(): SyncSource | null { return this._conflictsSource; } + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService + ) { + super(); + this.channel = sharedProcessService.getChannel('userDataSync'); + this._register(this.channel.listen('onDidChangeStatus')(status => this.updateStatus(status))); + } + + sync(_continue?: boolean): Promise { + return this.channel.call('sync', [_continue]); + } + + private async updateStatus(status: SyncStatus): Promise { + this._conflictsSource = await this.channel.call('getConflictsSource'); + this._status = status; + this._onDidChangeStatus.fire(status); + } + +} + +registerSingleton(IUserDataSyncService, UserDataSyncService); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 73fc63f220c..4526dbac1b2 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -79,7 +79,7 @@ import 'vs/workbench/services/label/common/labelService'; import 'vs/workbench/services/extensionManagement/common/extensionEnablementService'; import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/extensions/common/staticExtensions'; -import 'vs/workbench/services/userData/common/settingsMergeService'; +import 'vs/workbench/services/userDataSync/common/settingsMergeService'; import 'vs/workbench/services/workspace/browser/workspaceEditingService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -106,9 +106,6 @@ import { IDownloadService } from 'vs/platform/download/common/download'; import { DownloadService } from 'vs/platform/download/common/downloadService'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IUserDataSyncStoreService, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync'; -import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; registerSingleton(IExtensionGalleryService, ExtensionGalleryService, true); registerSingleton(IContextViewService, ContextViewService, true); @@ -122,8 +119,6 @@ registerSingleton(ITextResourceConfigurationService, TextResourceConfigurationSe registerSingleton(IMenuService, MenuService, true); registerSingleton(IDownloadService, DownloadService, true); registerSingleton(IOpenerService, OpenerService, true); -registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); -registerSingleton(IUserDataSyncService, UserDataSyncService); //#endregion @@ -259,7 +254,7 @@ import 'vs/workbench/contrib/experiments/browser/experiments.contribution'; // Send a Smile import 'vs/workbench/contrib/feedback/browser/feedback.contribution'; -// User Data -import 'vs/workbench/contrib/userData/browser/userData.contribution'; +// User Data Sync +import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution'; //#endregion diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index f22592cb8fe..e864ab64498 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -49,6 +49,7 @@ import 'vs/workbench/services/backup/node/backupFileService'; import 'vs/workbench/services/credentials/node/credentialsService'; import 'vs/workbench/services/url/electron-browser/urlService'; import 'vs/workbench/services/workspace/electron-browser/workspacesService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -137,4 +138,7 @@ import 'vs/workbench/contrib/issue/electron-browser/issue.contribution'; // Tasks import 'vs/workbench/contrib/tasks/electron-browser/taskService'; +// User Data Sync +import 'vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution'; + //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index dc8eb1bb5e6..65078676b4e 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -61,6 +61,9 @@ import { BackupFileService } from 'vs/workbench/services/backup/common/backupFil import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { NoOpTunnelService } from 'vs/platform/remote/common/tunnelService'; +import { IUserDataSyncStoreService, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; registerSingleton(IRequestService, RequestService, true); registerSingleton(IExtensionManagementService, ExtensionManagementService); @@ -70,6 +73,8 @@ registerSingleton(IAccessibilityService, BrowserAccessibilityService, true); registerSingleton(ILifecycleService, BrowserLifecycleService); registerSingleton(IContextMenuService, ContextMenuService); registerSingleton(ITunnelService, NoOpTunnelService, true); +registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); +registerSingleton(IUserDataSyncService, UserDataSyncService); //#endregion