UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

623 lines (622 loc) 23.9 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { VSBuffer } from '@sussudio/base/common/buffer.mjs'; import { getErrorMessage } from '@sussudio/base/common/errors.mjs'; import { Event } from '@sussudio/base/common/event.mjs'; import { parse } from '@sussudio/base/common/json.mjs'; import { toFormattedString } from '@sussudio/base/common/jsonFormatter.mjs'; import { isWeb } from '@sussudio/base/common/platform.mjs'; import { generateUuid } from '@sussudio/base/common/uuid.mjs'; import { IConfigurationService } from '../../configuration/common/configuration.mjs'; import { IEnvironmentService } from '../../environment/common/environment.mjs'; import { IFileService } from '../../files/common/files.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { getServiceMachineId } from '../../externalServices/common/serviceMachineId.mjs'; import { IStorageService } from '../../storage/common/storage.mjs'; import { ITelemetryService } from '../../telemetry/common/telemetry.mjs'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.mjs'; import { AbstractInitializer, AbstractSynchroniser, getSyncResourceLogLabel, isSyncData, } from './abstractSynchronizer.mjs'; import { edit } from './content.mjs'; import { merge } from './globalStateMerge.mjs'; import { ALL_SYNC_RESOURCES, createSyncHeaders, getEnablementKey, IUserDataSyncBackupStoreService, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SYNC_SERVICE_URL_TYPE, UserDataSyncError, USER_DATA_SYNC_SCHEME, } from './userDataSync.mjs'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.mjs'; import { IUserDataProfileStorageService } from '../../userDataProfile/common/userDataProfileStorageService.mjs'; import { IInstantiationService } from '../../instantiation/common/instantiation.mjs'; const argvStoragePrefx = 'globalState.argv.'; const argvProperties = ['locale']; export function stringify(globalState, format) { const storageKeys = globalState.storage ? Object.keys(globalState.storage).sort() : []; const storage = {}; storageKeys.forEach((key) => (storage[key] = globalState.storage[key])); globalState.storage = storage; return format ? toFormattedString(globalState, {}) : JSON.stringify(globalState); } const GLOBAL_STATE_DATA_VERSION = 1; /** * Synchronises global state that includes * - Global storage with user scope * - Locale from argv properties * * Global storage is synced without checking version just like other resources (settings, keybindings). * If there is a change in format of the value of a storage key which requires migration then * Owner of that key should remove that key from user scope and replace that with new user scoped key. */ let GlobalStateSynchroniser = class GlobalStateSynchroniser extends AbstractSynchroniser { userDataProfileStorageService; version = GLOBAL_STATE_DATA_VERSION; previewResource = this.extUri.joinPath(this.syncPreviewFolder, 'globalState.json'); baseResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' }); localResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }); remoteResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }); acceptedResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }); localGlobalStateProvider; constructor( profile, collection, userDataProfileStorageService, fileService, userDataSyncStoreService, userDataSyncBackupStoreService, logService, environmentService, userDataSyncEnablementService, telemetryService, configurationService, storageService, uriIdentityService, instantiationService, ) { super( { syncResource: 'globalState' /* SyncResource.GlobalState */, profile }, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService, ); this.userDataProfileStorageService = userDataProfileStorageService; this.localGlobalStateProvider = instantiationService.createInstance(LocalGlobalStateProvider); this._register(fileService.watch(this.extUri.dirname(this.environmentService.argvResource))); this._register( Event.any( /* Locale change */ Event.filter(fileService.onDidFilesChange, (e) => e.contains(this.environmentService.argvResource)), Event.filter(userDataProfileStorageService.onDidChange, (e) => { /* StorageTarget has changed in profile storage */ if (e.targetChanges.some((profile) => this.syncResource.profile.id === profile.id)) { return true; } /* User storage data has changed in profile storage */ if ( e.valueChanges.some( ({ profile, changes }) => this.syncResource.profile.id === profile.id && changes.some((change) => change.target === 0 /* StorageTarget.USER */), ) ) { return true; } return false; }), )(() => this.triggerLocalChange()), ); } async generateSyncPreview(remoteUserData, lastSyncUserData, isRemoteDataFromCurrentMachine) { const remoteGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; // Use remote data as last sync data if last sync data does not exist and remote data is from same machine lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData; const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; const localGlobalState = await this.localGlobalStateProvider.getLocalGlobalState(this.syncResource.profile); if (remoteGlobalState) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote ui state with local ui state...`); } else { this.logService.trace( `${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`, ); } const storageKeys = await this.getStorageKeys(lastSyncGlobalState); const { local, remote } = merge( localGlobalState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, storageKeys, this.logService, ); const previewResult = { content: null, local, remote, localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? 2 /* Change.Modified */ : 0 /* Change.None */, remoteChange: remote.all !== null ? 2 /* Change.Modified */ : 0 /* Change.None */, }; const localContent = stringify(localGlobalState, false); return [ { baseResource: this.baseResource, baseContent: lastSyncGlobalState ? stringify(lastSyncGlobalState, false) : localContent, localResource: this.localResource, localContent, localUserData: localGlobalState, remoteResource: this.remoteResource, remoteContent: remoteGlobalState ? stringify(remoteGlobalState, false) : null, previewResource: this.previewResource, previewResult, localChange: previewResult.localChange, remoteChange: previewResult.remoteChange, acceptedResource: this.acceptedResource, storageKeys, }, ]; } async hasRemoteChanged(lastSyncUserData) { const lastSyncGlobalState = lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; if (lastSyncGlobalState === null) { return true; } const localGlobalState = await this.localGlobalStateProvider.getLocalGlobalState(this.syncResource.profile); const storageKeys = await this.getStorageKeys(lastSyncGlobalState); const { remote } = merge( localGlobalState.storage, lastSyncGlobalState.storage, lastSyncGlobalState.storage, storageKeys, this.logService, ); return remote.all !== null; } async getMergeResult(resourcePreview, token) { return { ...resourcePreview.previewResult, hasConflicts: false }; } async getAcceptResult(resourcePreview, resource, content, token) { /* Accept local resource */ if (this.extUri.isEqual(resource, this.localResource)) { return this.acceptLocal(resourcePreview); } /* Accept remote resource */ if (this.extUri.isEqual(resource, this.remoteResource)) { return this.acceptRemote(resourcePreview); } /* Accept preview resource */ if (this.extUri.isEqual(resource, this.previewResource)) { return resourcePreview.previewResult; } throw new Error(`Invalid Resource: ${resource.toString()}`); } async acceptLocal(resourcePreview) { return { content: resourcePreview.localContent, local: { added: {}, removed: [], updated: {} }, remote: { added: Object.keys(resourcePreview.localUserData.storage), removed: [], updated: [], all: resourcePreview.localUserData.storage, }, localChange: 0 /* Change.None */, remoteChange: 2 /* Change.Modified */, }; } async acceptRemote(resourcePreview) { if (resourcePreview.remoteContent !== null) { const remoteGlobalState = JSON.parse(resourcePreview.remoteContent); const { local, remote } = merge( resourcePreview.localUserData.storage, remoteGlobalState.storage, null, resourcePreview.storageKeys, this.logService, ); return { content: resourcePreview.remoteContent, local, remote, localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? 2 /* Change.Modified */ : 0 /* Change.None */, remoteChange: remote !== null ? 2 /* Change.Modified */ : 0 /* Change.None */, }; } else { return { content: resourcePreview.remoteContent, local: { added: {}, removed: [], updated: {} }, remote: { added: [], removed: [], updated: [], all: null }, localChange: 0 /* Change.None */, remoteChange: 0 /* Change.None */, }; } } async applyResult(remoteUserData, lastSyncUserData, resourcePreviews, force) { const { localUserData } = resourcePreviews[0][0]; const { local, remote, localChange, remoteChange } = resourcePreviews[0][1]; if (localChange === 0 /* Change.None */ && remoteChange === 0 /* Change.None */) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`); } if (localChange !== 0 /* Change.None */) { // update local this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`); await this.backupLocal(JSON.stringify(localUserData)); await this.localGlobalStateProvider.writeLocalGlobalState(local, this.syncResource.profile); this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`); } if (remoteChange !== 0 /* Change.None */) { // update remote this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`); const content = JSON.stringify({ storage: remote.all }); remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref); this.logService.info( `${this.syncResourceLogLabel}: Updated remote ui state.${ remote.added.length ? ` Added: ${remote.added}.` : '' }${remote.updated.length ? ` Updated: ${remote.updated}.` : ''}${ remote.removed.length ? ` Removed: ${remote.removed}.` : '' }`, ); } if (lastSyncUserData?.ref !== remoteUserData.ref) { // update last sync this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`); await this.updateLastSyncUserData(remoteUserData); this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`); } } async resolveContent(uri) { if ( this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.baseResource, uri) || this.extUri.isEqual(this.localResource, uri) || this.extUri.isEqual(this.acceptedResource, uri) ) { const content = await this.resolvePreviewContent(uri); return content ? stringify(JSON.parse(content), true) : content; } return null; } async hasLocalData() { try { const { storage } = await this.localGlobalStateProvider.getLocalGlobalState(this.syncResource.profile); if (Object.keys(storage).length > 1 || storage[`${argvStoragePrefx}.locale`]?.value !== 'en') { return true; } } catch (error) { /* ignore error */ } return false; } async getStorageKeys(lastSyncGlobalState) { const storageData = await this.userDataProfileStorageService.readStorageData(this.syncResource.profile); const user = [], machine = []; for (const [key, value] of storageData) { if (value.target === 0 /* StorageTarget.USER */) { user.push(key); } else if (value.target === 1 /* StorageTarget.MACHINE */) { machine.push(key); } } const registered = [...user, ...machine]; const unregistered = lastSyncGlobalState?.storage ? Object.keys(lastSyncGlobalState.storage).filter( (key) => !key.startsWith(argvStoragePrefx) && !registered.includes(key) && storageData.get(key) !== undefined, ) : []; if (!isWeb) { // Following keys are synced only in web. Do not sync these keys in other platforms const keysSyncedOnlyInWeb = [ ...ALL_SYNC_RESOURCES.map((resource) => getEnablementKey(resource)), SYNC_SERVICE_URL_TYPE, ]; unregistered.push(...keysSyncedOnlyInWeb); machine.push(...keysSyncedOnlyInWeb); } return { user, machine, unregistered }; } }; GlobalStateSynchroniser = __decorate( [ __param(2, IUserDataProfileStorageService), __param(3, IFileService), __param(4, IUserDataSyncStoreService), __param(5, IUserDataSyncBackupStoreService), __param(6, IUserDataSyncLogService), __param(7, IEnvironmentService), __param(8, IUserDataSyncEnablementService), __param(9, ITelemetryService), __param(10, IConfigurationService), __param(11, IStorageService), __param(12, IUriIdentityService), __param(13, IInstantiationService), ], GlobalStateSynchroniser, ); export { GlobalStateSynchroniser }; let LocalGlobalStateProvider = class LocalGlobalStateProvider { fileService; environmentService; userDataProfileStorageService; logService; constructor(fileService, environmentService, userDataProfileStorageService, logService) { this.fileService = fileService; this.environmentService = environmentService; this.userDataProfileStorageService = userDataProfileStorageService; this.logService = logService; } async getLocalGlobalState(profile) { const storage = {}; if (profile.isDefault) { const argvContent = await this.getLocalArgvContent(); const argvValue = parse(argvContent); for (const argvProperty of argvProperties) { if (argvValue[argvProperty] !== undefined) { storage[`${argvStoragePrefx}${argvProperty}`] = { version: 1, value: argvValue[argvProperty] }; } } } const storageData = await this.userDataProfileStorageService.readStorageData(profile); for (const [key, value] of storageData) { if (value.value && value.target === 0 /* StorageTarget.USER */) { storage[key] = { version: 1, value: value.value }; } } return { storage }; } async getLocalArgvContent() { try { this.logService.debug('GlobalStateSync#getLocalArgvContent', this.environmentService.argvResource); const content = await this.fileService.readFile(this.environmentService.argvResource); this.logService.debug('GlobalStateSync#getLocalArgvContent - Resolved', this.environmentService.argvResource); return content.value.toString(); } catch (error) { this.logService.debug(getErrorMessage(error)); } return '{}'; } async writeLocalGlobalState({ added, removed, updated }, profile) { const syncResourceLogLabel = getSyncResourceLogLabel('globalState' /* SyncResource.GlobalState */, profile); const argv = {}; const updatedStorage = new Map(); const storageData = await this.userDataProfileStorageService.readStorageData(profile); const handleUpdatedStorage = (keys, storage) => { for (const key of keys) { if (key.startsWith(argvStoragePrefx)) { argv[key.substring(argvStoragePrefx.length)] = storage ? storage[key].value : undefined; continue; } if (storage) { const storageValue = storage[key]; if (storageValue.value !== storageData.get(key)?.value) { updatedStorage.set(key, storageValue.value); } } else { if (storageData.get(key) !== undefined) { updatedStorage.set(key, undefined); } } } }; handleUpdatedStorage(Object.keys(added), added); handleUpdatedStorage(Object.keys(updated), updated); handleUpdatedStorage(removed); if (Object.keys(argv).length) { this.logService.trace(`${syncResourceLogLabel}: Updating locale...`); const argvContent = await this.getLocalArgvContent(); let content = argvContent; for (const argvProperty of Object.keys(argv)) { content = edit(content, [argvProperty], argv[argvProperty], {}); } if (argvContent !== content) { this.logService.trace(`${syncResourceLogLabel}: Updating locale...`); await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content)); this.logService.info(`${syncResourceLogLabel}: Updated locale.`); } this.logService.info(`${syncResourceLogLabel}: Updated locale`); } if (updatedStorage.size) { this.logService.trace(`${syncResourceLogLabel}: Updating global state...`); await this.userDataProfileStorageService.updateStorageData(profile, updatedStorage, 0 /* StorageTarget.USER */); this.logService.info(`${syncResourceLogLabel}: Updated global state`, [...updatedStorage.keys()]); } } }; LocalGlobalStateProvider = __decorate( [ __param(0, IFileService), __param(1, IEnvironmentService), __param(2, IUserDataProfileStorageService), __param(3, IUserDataSyncLogService), ], LocalGlobalStateProvider, ); export { LocalGlobalStateProvider }; let GlobalStateInitializer = class GlobalStateInitializer extends AbstractInitializer { constructor( storageService, fileService, userDataProfilesService, environmentService, logService, uriIdentityService, ) { super( 'globalState' /* SyncResource.GlobalState */, userDataProfilesService, environmentService, logService, fileService, storageService, uriIdentityService, ); } async doInitialize(remoteUserData) { const remoteGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; if (!remoteGlobalState) { this.logService.info('Skipping initializing global state because remote global state does not exist.'); return; } const argv = {}; const storage = {}; for (const key of Object.keys(remoteGlobalState.storage)) { if (key.startsWith(argvStoragePrefx)) { argv[key.substring(argvStoragePrefx.length)] = remoteGlobalState.storage[key].value; } else { if (this.storageService.get(key, 0 /* StorageScope.PROFILE */) === undefined) { storage[key] = remoteGlobalState.storage[key].value; } } } if (Object.keys(argv).length) { let content = '{}'; try { const fileContent = await this.fileService.readFile(this.environmentService.argvResource); content = fileContent.value.toString(); } catch (error) {} for (const argvProperty of Object.keys(argv)) { content = edit(content, [argvProperty], argv[argvProperty], {}); } await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content)); } if (Object.keys(storage).length) { for (const key of Object.keys(storage)) { this.storageService.store(key, storage[key], 0 /* StorageScope.PROFILE */, 0 /* StorageTarget.USER */); } } } }; GlobalStateInitializer = __decorate( [ __param(0, IStorageService), __param(1, IFileService), __param(2, IUserDataProfilesService), __param(3, IEnvironmentService), __param(4, IUserDataSyncLogService), __param(5, IUriIdentityService), ], GlobalStateInitializer, ); export { GlobalStateInitializer }; let UserDataSyncStoreTypeSynchronizer = class UserDataSyncStoreTypeSynchronizer { userDataSyncStoreClient; storageService; environmentService; fileService; logService; constructor(userDataSyncStoreClient, storageService, environmentService, fileService, logService) { this.userDataSyncStoreClient = userDataSyncStoreClient; this.storageService = storageService; this.environmentService = environmentService; this.fileService = fileService; this.logService = logService; } getSyncStoreType(userData) { const remoteGlobalState = this.parseGlobalState(userData); return remoteGlobalState?.storage[SYNC_SERVICE_URL_TYPE]?.value; } async sync(userDataSyncStoreType) { const syncHeaders = createSyncHeaders(generateUuid()); try { return await this.doSync(userDataSyncStoreType, syncHeaders); } catch (e) { if (e instanceof UserDataSyncError) { switch (e.code) { case 'PreconditionFailed' /* UserDataSyncErrorCode.PreconditionFailed */: this.logService.info( `Failed to synchronize UserDataSyncStoreType as there is a new remote version available. Synchronizing again...`, ); return this.doSync(userDataSyncStoreType, syncHeaders); } } throw e; } } async doSync(userDataSyncStoreType, syncHeaders) { // Read the global state from remote const globalStateUserData = await this.userDataSyncStoreClient.readResource( 'globalState' /* SyncResource.GlobalState */, null, undefined, syncHeaders, ); const remoteGlobalState = this.parseGlobalState(globalStateUserData) || { storage: {} }; // Update the sync store type remoteGlobalState.storage[SYNC_SERVICE_URL_TYPE] = { value: userDataSyncStoreType, version: GLOBAL_STATE_DATA_VERSION, }; // Write the global state to remote const machineId = await getServiceMachineId(this.environmentService, this.fileService, this.storageService); const syncDataToUpdate = { version: GLOBAL_STATE_DATA_VERSION, machineId, content: stringify(remoteGlobalState, false), }; await this.userDataSyncStoreClient.writeResource( 'globalState' /* SyncResource.GlobalState */, JSON.stringify(syncDataToUpdate), globalStateUserData.ref, undefined, syncHeaders, ); } parseGlobalState({ content }) { if (!content) { return null; } const syncData = JSON.parse(content); if (isSyncData(syncData)) { return syncData ? JSON.parse(syncData.content) : null; } throw new Error('Invalid remote data'); } }; UserDataSyncStoreTypeSynchronizer = __decorate( [__param(1, IStorageService), __param(2, IEnvironmentService), __param(3, IFileService), __param(4, ILogService)], UserDataSyncStoreTypeSynchronizer, ); export { UserDataSyncStoreTypeSynchronizer };