提交 20601293 编写于 作者: S Sandeep Somavarapu

Enable syncing extensions storage

- Implement logic to sync extension storage
- Register keys to sync provided by extension
上级 9b507d2b
......@@ -5,7 +5,8 @@
import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { deepClone } from 'vs/base/common/objects';
import { deepClone, equals } from 'vs/base/common/objects';
import { IStringDictionary } from 'vs/base/common/collections';
export interface IMergeResult {
added: ISyncExtension[];
......@@ -72,6 +73,13 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet);
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
const mergeAndUpdate = (key: string): void => {
const extension = remoteExtensionsMap.get(key)!;
extension.state = mergeExtensionState(localExtensionsMap.get(key)?.state, extension.state, lastSyncExtensionsMap?.get(key)?.state);
updated.push(massageOutgoingExtension(extension, key));
newRemoteExtensionsMap.set(key, extension);
};
// Remotely removed extension.
for (const key of baseToRemote.removed.values()) {
const e = localExtensionsMap.get(key);
......@@ -86,7 +94,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
if (baseToLocal.added.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
mergeAndUpdate(key);
}
} else {
// Add only installed extension to local
......@@ -100,7 +108,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
// Remotely updated extensions
for (const key of baseToRemote.updated.values()) {
// Update in local always
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
mergeAndUpdate(key);
}
// Locally added extensions
......@@ -165,7 +173,7 @@ function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISync
const toExtension = to.get(key);
if (!toExtension
|| fromExtension.disabled !== toExtension.disabled
|| fromExtension.version !== toExtension.version
|| !isSameExtensionState(fromExtension.state, toExtension.state)
|| (checkInstalledProperty && fromExtension.installed !== toExtension.installed)
) {
updated.add(key);
......@@ -175,6 +183,64 @@ function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISync
return { added, removed, updated };
}
function mergeExtensionState(local: IStringDictionary<any> | undefined, remote: IStringDictionary<any> | undefined, base: IStringDictionary<any> | undefined): IStringDictionary<any> | undefined {
if (!local && !remote && !base) {
return undefined;
}
if (local && !remote && !base) {
return local;
}
if (remote && !local && !base) {
return remote;
}
local = local || {};
const merged: IStringDictionary<any> = deepClone(local);
if (remote) {
const baseToRemote = base ? compareExtensionState(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToLocal = base ? compareExtensionState(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
// Added/Updated in remote
for (const key of [...baseToRemote.added.values(), ...baseToRemote.updated.values()]) {
merged[key] = remote[key];
}
// Removed in remote
for (const key of baseToRemote.removed.values()) {
// Not updated in local
if (!baseToLocal.updated.has(key)) {
delete merged[key];
}
}
}
return merged;
}
function compareExtensionState(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = Object.keys(from);
const toKeys = Object.keys(to);
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
for (const key of fromKeys) {
if (removed.has(key)) {
continue;
}
const value1 = from[key];
const value2 = to[key];
if (!equals(value1, value2)) {
updated.add(key);
}
}
return { added, removed, updated };
}
function isSameExtensionState(a: IStringDictionary<any> = {}, b: IStringDictionary<any> = {}): boolean {
const { added, removed, updated } = compareExtensionState(a, b);
return added.size === 0 && removed.size === 0 && updated.size === 0;
}
// massage incoming extension - add optional properties
function massageIncomingExtension(extension: ISyncExtension): ISyncExtension {
return { ...extension, ...{ disabled: !!extension.disabled, installed: !!extension.installed } };
......
......@@ -21,9 +21,12 @@ import { URI } from 'vs/base/common/uri';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
import { compare } from 'vs/base/common/strings';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { getErrorMessage } from 'vs/base/common/errors';
import { forEach } from 'vs/base/common/collections';
interface IExtensionResourceMergeResult extends IAcceptResult {
readonly added: ISyncExtension[];
......@@ -79,7 +82,8 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
protected readonly version: number = 3;
*/
/* Version 4: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
protected readonly version: number = 4;
/* Version 5: Introduce extension state */
protected readonly version: number = 5;
protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); }
private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'extensions.json');
......@@ -90,7 +94,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
......@@ -101,6 +105,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
@IConfigurationService configurationService: IConfigurationService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(SyncResource.Extensions, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
this._register(
......@@ -354,8 +359,13 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
await Promise.all([...added, ...updated].map(async e => {
const installedExtension = installedExtensions.filter(installed => areSameExtensions(installed.identifier, e.identifier))[0];
// Builtin Extension: Sync only enablement state
// Builtin Extension: Sync enablement and state
if (installedExtension && installedExtension.isBuiltin) {
if (e.state) {
const extensionState = JSON.parse(this.storageService.get(e.identifier.id, StorageScope.GLOBAL) || '{}');
forEach(e.state, ({ key, value }) => extensionState[key] = value);
this.storageService.store(e.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL);
}
if (e.disabled) {
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id);
await this.extensionEnablementService.disableExtension(e.identifier);
......@@ -369,9 +379,19 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return;
}
const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier, e.version);
const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier);
if (extension) {
try {
/* Update extension state only if
* extension is not installed or
* installed extension is same version as synced version
*/
if (e.state && (!installedExtension || installedExtension.manifest.version === e.version)) {
const extensionState = JSON.parse(this.storageService.get(extension.identifier.id, StorageScope.GLOBAL) || '{}');
forEach(e.state, ({ key, value }) => extensionState[key] = value);
this.storageService.store(e.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL);
}
if (e.disabled) {
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id, extension.version);
await this.extensionEnablementService.disableExtension(extension.identifier);
......@@ -381,8 +401,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
await this.extensionEnablementService.enableExtension(extension.identifier);
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id, extension.version);
}
// Install only if the extension does not exist
if (!installedExtension || installedExtension.manifest.version !== extension.version) {
if (!installedExtension) {
this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...`, e.identifier.id, extension.version);
await this.extensionManagementService.installFromGallery(extension);
this.logService.info(`${this.syncResourceLogLabel}: Installed extension.`, e.identifier.id, extension.version);
......@@ -420,14 +441,23 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
private getLocalExtensions(installedExtensions: ILocalExtension[]): ISyncExtension[] {
const disabledExtensions = this.extensionEnablementService.getDisabledExtensions();
return installedExtensions
.map(({ identifier, isBuiltin }) => {
const syncExntesion: ISyncExtension = { identifier };
.map(({ identifier, isBuiltin, manifest }) => {
const syncExntesion: ISyncExtension = { identifier, version: manifest.version };
if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) {
syncExntesion.disabled = true;
}
if (!isBuiltin) {
syncExntesion.installed = true;
}
const keys = this.storageKeysSyncRegistryService.getExtensioStorageKeys({ id: identifier.id, version: manifest.version });
try {
const extensionStorageValue = this.storageService.get(identifier.id, StorageScope.GLOBAL);
syncExntesion.state = keys.length && extensionStorageValue
? JSON.parse(extensionStorageValue, (key, value) => !key || keys.includes(key) ? value : undefined)
: undefined;
} catch (error) {
this.logService.info(`${this.syncResourceLogLabel}: Error while parsing extension state`, getErrorMessage(error));
}
return syncExntesion;
});
}
......@@ -440,6 +470,7 @@ export class ExtensionsInitializer extends AbstractInitializer {
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
@IStorageService private readonly storageService: IStorageService,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
......@@ -455,15 +486,19 @@ export class ExtensionsInitializer extends AbstractInitializer {
}
const installedExtensions = await this.extensionManagementService.getInstalled();
const newExtensionsToSync = new Map<string, ISyncExtension>();
const installedExtensionsToSync: ISyncExtension[] = [];
const toInstall: { names: string[], uuids: string[] } = { names: [], uuids: [] };
const toDisable: IExtensionIdentifier[] = [];
for (const extension of remoteExtensions) {
if (installedExtensions.some(i => areSameExtensions(i.identifier, extension.identifier))) {
installedExtensionsToSync.push(extension);
if (extension.disabled) {
toDisable.push(extension.identifier);
}
} else {
if (extension.installed) {
newExtensionsToSync.set(extension.identifier.id.toLowerCase(), extension);
if (extension.identifier.uuid) {
toInstall.uuids.push(extension.identifier.uuid);
} else {
......@@ -477,6 +512,10 @@ export class ExtensionsInitializer extends AbstractInitializer {
const galleryExtensions = (await this.galleryService.query({ ids: toInstall.uuids, names: toInstall.names, pageSize: toInstall.uuids.length + toInstall.names.length }, CancellationToken.None)).firstPage;
for (const galleryExtension of galleryExtensions) {
try {
const extensionToSync = newExtensionsToSync.get(galleryExtension.identifier.id.toLowerCase())!;
if (extensionToSync.state) {
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionToSync.state), StorageScope.GLOBAL);
}
this.logService.trace(`Installing extension...`, galleryExtension.identifier.id);
await this.extensionManagementService.installFromGallery(galleryExtension);
this.logService.info(`Installed extension.`, galleryExtension.identifier.id);
......@@ -493,6 +532,14 @@ export class ExtensionsInitializer extends AbstractInitializer {
this.logService.info(`Enabled extension`, identifier.id);
}
}
for (const extensionToSync of installedExtensionsToSync) {
if (extensionToSync.state) {
const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}');
forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value);
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL);
}
}
}
}
......
......@@ -289,6 +289,7 @@ export interface ISyncExtension {
version?: string;
disabled?: boolean;
installed?: boolean;
state?: IStringDictionary<any>;
}
export interface IStorageValue {
......
......@@ -7,20 +7,24 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
import { MainThreadStorageShape, MainContext, IExtHostContext, ExtHostStorageShape, ExtHostContext } from '../common/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IExtensionIdWithVersion, IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
@extHostNamedCustomer(MainContext.MainThreadStorage)
export class MainThreadStorage implements MainThreadStorageShape {
private readonly _storageService: IStorageService;
private readonly _storageKeysSyncRegistryService: IStorageKeysSyncRegistryService;
private readonly _proxy: ExtHostStorageShape;
private readonly _storageListener: IDisposable;
private readonly _sharedStorageKeysToWatch: Map<string, boolean> = new Map<string, boolean>();
constructor(
extHostContext: IExtHostContext,
@IStorageService storageService: IStorageService
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
this._storageService = storageService;
this._storageKeysSyncRegistryService = storageKeysSyncRegistryService;
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostStorage);
this._storageListener = this._storageService.onDidChangeStorage(e => {
......@@ -68,4 +72,8 @@ export class MainThreadStorage implements MainThreadStorageShape {
}
return Promise.resolve(undefined);
}
$registerExtensionStorageKeysToSync(extension: IExtensionIdWithVersion, keys: string[]): void {
this._storageKeysSyncRegistryService.registerExtensionStorageKeys(extension, keys);
}
}
......@@ -57,6 +57,7 @@ import { Dto } from 'vs/base/common/types';
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes';
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/storageKeys';
export interface IEnvironment {
isExtensionDevelopmentDebug: boolean;
......@@ -571,6 +572,7 @@ export interface MainThreadStatusBarShape extends IDisposable {
export interface MainThreadStorageShape extends IDisposable {
$getValue<T>(shared: boolean, key: string): Promise<T | undefined>;
$setValue(shared: boolean, key: string, value: object): Promise<void>;
$registerExtensionStorageKeysToSync(extension: IExtensionIdWithVersion, keys: string[]): void;
}
export interface MainThreadTelemetryShape extends IDisposable {
......
......@@ -371,8 +371,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise<vscode.ExtensionContext> {
const globalState = new ExtensionMemento(extensionDescription.identifier.value, true, this._storage);
const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage);
const globalState = new ExtensionMemento(extensionDescription, true, this._storage);
const workspaceState = new ExtensionMemento(extensionDescription, false, this._storage);
const extensionMode = extensionDescription.isUnderDevelopment
? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development)
: ExtensionMode.Production;
......
......@@ -6,10 +6,12 @@
import type * as vscode from 'vscode';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
export class ExtensionMemento implements vscode.Memento {
private readonly _id: string;
private readonly _version: string;
private readonly _shared: boolean;
private readonly _storage: ExtHostStorage;
......@@ -17,8 +19,16 @@ export class ExtensionMemento implements vscode.Memento {
private _value?: { [n: string]: any; };
private readonly _storageListener: IDisposable;
constructor(id: string, global: boolean, storage: ExtHostStorage) {
this._id = id;
private _syncKeys: string[] = [];
get syncKeys(): ReadonlyArray<string> { return Object.freeze(this._syncKeys); }
set syncKeys(syncKeys: ReadonlyArray<string>) {
this._syncKeys = [...syncKeys];
this._storage.registerExtensionStorageKeysToSync({ id: this._id, version: this._version }, this._syncKeys);
}
constructor(extensionDescription: IExtensionDescription, global: boolean, storage: ExtHostStorage) {
this._id = extensionDescription.identifier.value;
this._version = extensionDescription.version;
this._shared = global;
this._storage = storage;
......
......@@ -7,6 +7,7 @@ import { MainContext, MainThreadStorageShape, ExtHostStorageShape } from './extH
import { Emitter } from 'vs/base/common/event';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/storageKeys';
export interface IStorageChangeEvent {
shared: boolean;
......@@ -27,6 +28,10 @@ export class ExtHostStorage implements ExtHostStorageShape {
this._proxy = mainContext.getProxy(MainContext.MainThreadStorage);
}
registerExtensionStorageKeysToSync(extension: IExtensionIdWithVersion, keys: string[]): void {
this._proxy.$registerExtensionStorageKeysToSync(extension, keys);
}
getValue<T>(shared: boolean, key: string, defaultValue?: T): Promise<T | undefined> {
return this._proxy.$getValue<T>(shared, key).then(value => value || defaultValue);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册