diff --git a/src/vs/base/browser/indexedDB.ts b/src/vs/base/browser/indexedDB.ts index 5fba9c0ab4c94..256519321240a 100644 --- a/src/vs/base/browser/indexedDB.ts +++ b/src/vs/base/browser/indexedDB.ts @@ -28,7 +28,7 @@ export class IndexedDB { return new IndexedDB(database, name); } - static async openDatabase(name: string, version: number | undefined, stores: string[]): Promise { + private static async openDatabase(name: string, version: number | undefined, stores: string[]): Promise { mark(`code/willOpenDatabase/${name}`); try { return await IndexedDB.doOpenDatabase(name, version, stores); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 218d0c3031409..8fb281645b92a 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -70,7 +70,7 @@ import { SharedProcess } from 'vs/platform/sharedProcess/electron-main/sharedPro import { ISignService } from 'vs/platform/sign/common/sign'; import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { StorageDatabaseChannel } from 'vs/platform/storage/electron-main/storageIpc'; -import { GlobalStorageMainService, IGlobalStorageMainService, IStorageMainService, StorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { ApplicationStorageMainService, IApplicationStorageMainService, IStorageMainService, StorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { ITelemetryService, machineIdKey, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; @@ -648,7 +648,7 @@ export class CodeApplication extends Disposable { // Storage services.set(IStorageMainService, new SyncDescriptor(StorageMainService)); - services.set(IGlobalStorageMainService, new SyncDescriptor(GlobalStorageMainService)); + services.set(IApplicationStorageMainService, new SyncDescriptor(ApplicationStorageMainService)); // External terminal if (isWindows) { diff --git a/src/vs/platform/externalServices/common/serviceMachineId.ts b/src/vs/platform/externalServices/common/serviceMachineId.ts index e2e17317b5e16..b6422b8b25a40 100644 --- a/src/vs/platform/externalServices/common/serviceMachineId.ts +++ b/src/vs/platform/externalServices/common/serviceMachineId.ts @@ -10,7 +10,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export async function getServiceMachineId(environmentService: IEnvironmentService, fileService: IFileService, storageService: IStorageService | undefined): Promise { - let uuid: string | null = storageService ? storageService.get('storage.serviceMachineId', StorageScope.GLOBAL) || null : null; + let uuid: string | null = storageService ? storageService.get('storage.serviceMachineId', StorageScope.APPLICATION) || null : null; if (uuid) { return uuid; } @@ -31,7 +31,7 @@ export async function getServiceMachineId(environmentService: IEnvironmentServic } } if (storageService) { - storageService.store('storage.serviceMachineId', uuid, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('storage.serviceMachineId', uuid, StorageScope.APPLICATION, StorageTarget.MACHINE); } return uuid; } diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 99e8716173c21..612aa17942680 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -45,9 +45,16 @@ export enum NeverShowAgainScope { WORKSPACE, /** - * Will never show this notification on any workspace again. + * Will never show this notification on any workspace of the same + * profile again. */ - GLOBAL + GLOBAL, + + /** + * Will never show this notification on any workspace across all + * profiles again. + */ + APPLICATION } export interface INeverShowAgainOptions { @@ -65,7 +72,8 @@ export interface INeverShowAgainOptions { /** * Whether to persist the choice in the current workspace or for all workspaces. By - * default it will be persisted for all workspaces (= `NeverShowAgainScope.GLOBAL`). + * default it will be persisted for all workspaces across all profiles + * (= `NeverShowAgainScope.APPLICATION`). */ readonly scope?: NeverShowAgainScope; } diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 645049cb7c265..63b7cf9e7b2e6 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -12,76 +12,115 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { InMemoryStorageDatabase, isStorageItemsChangeEvent, IStorage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractStorageService, IS_NEW_KEY, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; export class BrowserStorageService extends AbstractStorageService { private static BROWSER_DEFAULT_FLUSH_INTERVAL = 5 * 1000; // every 5s because async operations are not permitted on shutdown + private applicationStorage: IStorage | undefined; private globalStorage: IStorage | undefined; private workspaceStorage: IStorage | undefined; + private applicationStorageDatabase: IIndexedDBStorageDatabase | undefined; private globalStorageDatabase: IIndexedDBStorageDatabase | undefined; private workspaceStorageDatabase: IIndexedDBStorageDatabase | undefined; get hasPendingUpdate(): boolean { - return Boolean(this.globalStorageDatabase?.hasPendingUpdate || this.workspaceStorageDatabase?.hasPendingUpdate); + return Boolean( + this.applicationStorageDatabase?.hasPendingUpdate || + this.globalStorageDatabase?.hasPendingUpdate || + this.workspaceStorageDatabase?.hasPendingUpdate + ); } constructor( private readonly payload: IAnyWorkspaceIdentifier, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IUserDataProfilesService private readonly userDataProfileService: IUserDataProfilesService ) { super({ flushInterval: BrowserStorageService.BROWSER_DEFAULT_FLUSH_INTERVAL }); } private getId(scope: StorageScope): string { - return scope === StorageScope.GLOBAL ? 'global' : this.payload.id; + switch (scope) { + case StorageScope.APPLICATION: + return 'global'; // use the default profile global DB for application scope + case StorageScope.GLOBAL: + if (this.userDataProfileService.currentProfile.isDefault) { + return 'global'; // default profile DB has a fixed name for backwards compatibility + } else { + return `global-${this.userDataProfileService.currentProfile.id}`; + } + case StorageScope.WORKSPACE: + return this.payload.id; + } } protected async doInitialize(): Promise { // Create Storage in Parallel - const [workspaceStorageDatabase, globalStorageDatabase] = await Promises.settled([ - IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.WORKSPACE) }, this.logService), - IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.GLOBAL), broadcastChanges: true /* only for global storage */ }, this.logService) - ]); + const promises: Promise[] = []; + promises.push(IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.APPLICATION), broadcastChanges: true }, this.logService)); + promises.push(IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.WORKSPACE) }, this.logService)); + if (!this.userDataProfileService.currentProfile.isDefault) { + + // If we are in default profile, the global storage is + // actually the same as application storage. As such we + // avoid creating the storage library a second time on + // the same DB. + + promises.push(IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.GLOBAL), broadcastChanges: true }, this.logService)); + } + const [applicationStorageDatabase, workspaceStorageDatabase, globalStorageDatabase] = await Promises.settled(promises); // Workspace Storage this.workspaceStorageDatabase = this._register(workspaceStorageDatabase); this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase)); this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key))); + // Application Storage + this.applicationStorageDatabase = this._register(applicationStorageDatabase); + this.applicationStorage = this._register(new Storage(this.applicationStorageDatabase)); + this._register(this.applicationStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.APPLICATION, key))); + // Global Storage - this.globalStorageDatabase = this._register(globalStorageDatabase); - this.globalStorage = this._register(new Storage(this.globalStorageDatabase)); + if (globalStorageDatabase) { + this.globalStorageDatabase = this._register(globalStorageDatabase); + this.globalStorage = this._register(new Storage(this.globalStorageDatabase)); + } else { + this.globalStorage = this.applicationStorage; + } this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key))); - // Init both + // Init storages await Promises.settled([ this.workspaceStorage.init(), - this.globalStorage.init() + this.globalStorage.init(), + this.applicationStorage.init() ]); - // Check to see if this is the first time we are "opening" the application - const firstOpen = this.globalStorage.getBoolean(IS_NEW_KEY); - if (firstOpen === undefined) { - this.globalStorage.set(IS_NEW_KEY, true); - } else if (firstOpen) { - this.globalStorage.set(IS_NEW_KEY, false); - } - - // Check to see if this is the first time we are "opening" this workspace - const firstWorkspaceOpen = this.workspaceStorage.getBoolean(IS_NEW_KEY); - if (firstWorkspaceOpen === undefined) { - this.workspaceStorage.set(IS_NEW_KEY, true); - } else if (firstWorkspaceOpen) { - this.workspaceStorage.set(IS_NEW_KEY, false); + // Apply is-new markers + for (const storage of [this.applicationStorage, this.globalStorage, this.workspaceStorage]) { + const firstOpen = storage.getBoolean(IS_NEW_KEY); + if (firstOpen === undefined) { + storage.set(IS_NEW_KEY, true); + } else if (firstOpen) { + storage.set(IS_NEW_KEY, false); + } } } protected getStorage(scope: StorageScope): IStorage | undefined { - return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage; + switch (scope) { + case StorageScope.APPLICATION: + return this.applicationStorage; + case StorageScope.GLOBAL: + return this.globalStorage; + default: + return this.workspaceStorage; + } } protected getLogDetails(scope: StorageScope): string | undefined { @@ -115,6 +154,7 @@ export class BrowserStorageService extends AbstractStorageService { // On all other browsers, we keep the databases opened because // we expect data to be written when the unload happens. if (isSafari) { + this.applicationStorage?.close(); this.globalStorageDatabase?.close(); this.workspaceStorageDatabase?.close(); } @@ -127,7 +167,7 @@ export class BrowserStorageService extends AbstractStorageService { async clear(): Promise { // Clear key/values - for (const scope of [StorageScope.GLOBAL, StorageScope.WORKSPACE]) { + for (const scope of [StorageScope.APPLICATION, StorageScope.GLOBAL, StorageScope.WORKSPACE]) { for (const target of [StorageTarget.USER, StorageTarget.MACHINE]) { for (const key of this.keys(scope, target)) { this.remove(key, scope); @@ -139,6 +179,7 @@ export class BrowserStorageService extends AbstractStorageService { // Clear databases await Promises.settled([ + this.applicationStorageDatabase?.clear() ?? Promise.resolve(), this.globalStorageDatabase?.clear() ?? Promise.resolve(), this.workspaceStorageDatabase?.clear() ?? Promise.resolve() ]); diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 7d4ce4f359a43..9061792183db6 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -31,7 +31,7 @@ export enum WillSaveStateReason { } export interface IWillSaveStateEvent { - reason: WillSaveStateReason; + readonly reason: WillSaveStateReason; } export interface IStorageService { @@ -68,7 +68,7 @@ export interface IStorageService { * the provided `defaultValue` if the element is `null` or `undefined`. * * @param scope allows to define the scope of the storage operation - * to either the current workspace only or all workspaces. + * to either the current workspace only, all workspaces or all profiles. */ get(key: string, scope: StorageScope, fallbackValue: string): string; get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined; @@ -79,7 +79,7 @@ export interface IStorageService { * The element will be converted to a `boolean`. * * @param scope allows to define the scope of the storage operation - * to either the current workspace only or all workspaces. + * to either the current workspace only, all workspaces or all profiles. */ getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean; getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined; @@ -91,7 +91,7 @@ export interface IStorageService { * base of `10`. * * @param scope allows to define the scope of the storage operation - * to either the current workspace only or all workspaces. + * to either the current workspace only, all workspaces or all profiles. */ getNumber(key: string, scope: StorageScope, fallbackValue: number): number; getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined; @@ -102,7 +102,7 @@ export interface IStorageService { * remove the entry under the key. * * @param scope allows to define the scope of the storage operation - * to either the current workspace only or all workspaces. + * to either the current workspace only, all workspaces or all profiles. * * @param target allows to define the target of the storage operation * to either the current machine or user. @@ -113,7 +113,8 @@ export interface IStorageService { * Delete an element stored under the provided key from storage. * * The scope argument allows to define the scope of the storage - * operation to either the current workspace only or all workspaces. + * operation to either the current workspace only, all workspaces + * or all profiles. */ remove(key: string, scope: StorageScope): void; @@ -126,7 +127,7 @@ export interface IStorageService { * will be excluded from the results. * * @param scope allows to define the scope for the keys - * to either the current workspace only or all workspaces. + * to either the current workspace only, all workspaces or all profiles. * * @param target allows to define the target for the keys * to either the current machine or user. @@ -163,14 +164,19 @@ export interface IStorageService { export const enum StorageScope { /** - * The stored data will be scoped to all workspaces. + * The stored data will be scoped to all workspaces across all profiles. */ - GLOBAL, + APPLICATION = -1, + + /** + * The stored data will be scoped to all workspaces of the same profile. + */ + GLOBAL = 0, /** * The stored data will be scoped to the current workspace. */ - WORKSPACE + WORKSPACE = 1 } export const enum StorageTarget { @@ -302,10 +308,16 @@ export abstract class AbstractStorageService extends Disposable implements IStor if (key === TARGET_KEY) { // Clear our cached version which is now out of date - if (scope === StorageScope.GLOBAL) { - this._globalKeyTargets = undefined; - } else if (scope === StorageScope.WORKSPACE) { - this._workspaceKeyTargets = undefined; + switch (scope) { + case StorageScope.APPLICATION: + this._applicationKeyTargets = undefined; + break; + case StorageScope.GLOBAL: + this._globalKeyTargets = undefined; + break; + case StorageScope.WORKSPACE: + this._workspaceKeyTargets = undefined; + break; } // Emit as `didChangeTarget` event @@ -440,8 +452,24 @@ export abstract class AbstractStorageService extends Disposable implements IStor return this._globalKeyTargets; } + private _applicationKeyTargets: IKeyTargets | undefined = undefined; + private get applicationKeyTargets(): IKeyTargets { + if (!this._applicationKeyTargets) { + this._applicationKeyTargets = this.loadKeyTargets(StorageScope.APPLICATION); + } + + return this._applicationKeyTargets; + } + private getKeyTargets(scope: StorageScope): IKeyTargets { - return scope === StorageScope.GLOBAL ? this.globalKeyTargets : this.workspaceKeyTargets; + switch (scope) { + case StorageScope.APPLICATION: + return this.applicationKeyTargets; + case StorageScope.GLOBAL: + return this.globalKeyTargets; + default: + return this.workspaceKeyTargets; + } } private loadKeyTargets(scope: StorageScope): { [key: string]: StorageTarget } { @@ -466,6 +494,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor // Signal event to collect changes this._onWillSaveState.fire({ reason }); + const applicationStorage = this.getStorage(StorageScope.APPLICATION); const globalStorage = this.getStorage(StorageScope.GLOBAL); const workspaceStorage = this.getStorage(StorageScope.WORKSPACE); @@ -474,6 +503,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor // Unspecific reason: just wait when data is flushed case WillSaveStateReason.NONE: await Promises.settled([ + applicationStorage?.whenFlushed() ?? Promise.resolve(), globalStorage?.whenFlushed() ?? Promise.resolve(), workspaceStorage?.whenFlushed() ?? Promise.resolve() ]); @@ -483,6 +513,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor // and not hit any delays that might be there case WillSaveStateReason.SHUTDOWN: await Promises.settled([ + applicationStorage?.flush(0) ?? Promise.resolve(), globalStorage?.flush(0) ?? Promise.resolve(), workspaceStorage?.flush(0) ?? Promise.resolve() ]); @@ -491,12 +522,15 @@ export abstract class AbstractStorageService extends Disposable implements IStor } async logStorage(): Promise { + const applicationItems = this.getStorage(StorageScope.APPLICATION)?.items ?? new Map(); const globalItems = this.getStorage(StorageScope.GLOBAL)?.items ?? new Map(); const workspaceItems = this.getStorage(StorageScope.WORKSPACE)?.items ?? new Map(); return logStorage( + applicationItems, globalItems, workspaceItems, + this.getLogDetails(StorageScope.APPLICATION) ?? '', this.getLogDetails(StorageScope.GLOBAL) ?? '', this.getLogDetails(StorageScope.WORKSPACE) ?? '' ); @@ -515,6 +549,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor export class InMemoryStorageService extends AbstractStorageService { + private readonly applicationStorage = this._register(new Storage(new InMemoryStorageDatabase())); private readonly globalStorage = this._register(new Storage(new InMemoryStorageDatabase())); private readonly workspaceStorage = this._register(new Storage(new InMemoryStorageDatabase())); @@ -523,14 +558,29 @@ export class InMemoryStorageService extends AbstractStorageService { this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key))); this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key))); + this._register(this.applicationStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.APPLICATION, key))); } protected getStorage(scope: StorageScope): IStorage { - return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage; + switch (scope) { + case StorageScope.APPLICATION: + return this.applicationStorage; + case StorageScope.GLOBAL: + return this.globalStorage; + default: + return this.workspaceStorage; + } } protected getLogDetails(scope: StorageScope): string | undefined { - return scope === StorageScope.GLOBAL ? 'inMemory (global)' : 'inMemory (workspace)'; + switch (scope) { + case StorageScope.APPLICATION: + return 'inMemory (application)'; + case StorageScope.GLOBAL: + return 'inMemory (global)'; + default: + return 'inMemory (workspace)'; + } } protected async doInitialize(): Promise { } @@ -540,7 +590,7 @@ export class InMemoryStorageService extends AbstractStorageService { } } -export async function logStorage(global: Map, workspace: Map, globalPath: string, workspacePath: string): Promise { +export async function logStorage(application: Map, global: Map, workspace: Map, applicationPath: string, globalPath: string, workspacePath: string): Promise { const safeParse = (value: string) => { try { return JSON.parse(value); @@ -549,6 +599,13 @@ export async function logStorage(global: Map, workspace: Map(); + const applicationItemsParsed = new Map(); + application.forEach((value, key) => { + applicationItems.set(key, value); + applicationItemsParsed.set(key, safeParse(value)); + }); + const globalItems = new Map(); const globalItemsParsed = new Map(); global.forEach((value, key) => { @@ -563,15 +620,31 @@ export async function logStorage(global: Map, workspace: Map { - globalValues.push({ key, value }); + if (applicationPath !== globalPath) { + console.group(`Storage: Application (path: ${applicationPath})`); + } else { + console.group(`Storage: Application & Global (path: ${applicationPath}, default profile)`); + } + const applicationValues: { key: string; value: string }[] = []; + applicationItems.forEach((value, key) => { + applicationValues.push({ key, value }); }); - console.table(globalValues); + console.table(applicationValues); console.groupEnd(); - console.log(globalItemsParsed); + console.log(applicationItemsParsed); + + if (applicationPath !== globalPath) { + console.group(`Storage: Global (path: ${globalPath}, profile specific)`); + const globalValues: { key: string; value: string }[] = []; + globalItems.forEach((value, key) => { + globalValues.push({ key, value }); + }); + console.table(globalValues); + console.groupEnd(); + + console.log(globalItemsParsed); + } console.group(`Storage: Workspace (path: ${workspacePath})`); const workspaceValues: { key: string; value: string }[] = []; diff --git a/src/vs/platform/storage/common/storageIpc.ts b/src/vs/platform/storage/common/storageIpc.ts index 5774cb820853b..e61a2736999f8 100644 --- a/src/vs/platform/storage/common/storageIpc.ts +++ b/src/vs/platform/storage/common/storageIpc.ts @@ -7,6 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; +import { IUserDataProfileDto, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ISerializedSingleFolderWorkspaceIdentifier, ISerializedWorkspaceIdentifier, IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; export type Key = string; @@ -14,6 +15,18 @@ export type Value = string; export type Item = [Key, Value]; export interface IBaseSerializableStorageRequest { + + /** + * Profile to correlate storage. Only used when no + * workspace is provided. Can be undefined to denote + * application scope. + */ + readonly profile: IUserDataProfileDto | undefined; + + /** + * Workspace to correlate storage. Can be undefined to + * denote application or global scope depending on profile. + */ readonly workspace: ISerializedWorkspaceIdentifier | ISerializedSingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined; } @@ -31,19 +44,23 @@ abstract class BaseStorageDatabaseClient extends Disposable implements IStorageD abstract readonly onDidChangeItemsExternal: Event; - constructor(protected channel: IChannel, protected workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined) { + constructor( + protected channel: IChannel, + protected profile: IUserDataProfileDto | undefined, + protected workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined + ) { super(); } async getItems(): Promise> { - const serializableRequest: IBaseSerializableStorageRequest = { workspace: this.workspace }; + const serializableRequest: IBaseSerializableStorageRequest = { profile: this.profile, workspace: this.workspace }; const items: Item[] = await this.channel.call('getItems', serializableRequest); return new Map(items); } updateItems(request: IUpdateRequest): Promise { - const serializableRequest: ISerializableUpdateRequest = { workspace: this.workspace }; + const serializableRequest: ISerializableUpdateRequest = { profile: this.profile, workspace: this.workspace }; if (request.insert) { serializableRequest.insert = Array.from(request.insert.entries()); @@ -59,22 +76,22 @@ abstract class BaseStorageDatabaseClient extends Disposable implements IStorageD abstract close(): Promise; } -class GlobalStorageDatabaseClient extends BaseStorageDatabaseClient implements IStorageDatabase { +abstract class BaseProfileAwareStorageDatabaseClient extends BaseStorageDatabaseClient { private readonly _onDidChangeItemsExternal = this._register(new Emitter()); readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; - constructor(channel: IChannel) { - super(channel, undefined); + constructor(channel: IChannel, profile: IUserDataProfileDto | undefined) { + super(channel, profile, undefined); this.registerListeners(); } private registerListeners(): void { - this._register(this.channel.listen('onDidChangeGlobalStorage')((e: ISerializableItemsChangeEvent) => this.onDidChangeGlobalStorage(e))); + this._register(this.channel.listen('onDidChangeStorage', { profile: this.profile })((e: ISerializableItemsChangeEvent) => this.onDidChangeStorage(e))); } - private onDidChangeGlobalStorage(e: ISerializableItemsChangeEvent): void { + private onDidChangeStorage(e: ISerializableItemsChangeEvent): void { if (Array.isArray(e.changed) || Array.isArray(e.deleted)) { this._onDidChangeItemsExternal.fire({ changed: e.changed ? new Map(e.changed) : undefined, @@ -82,10 +99,17 @@ class GlobalStorageDatabaseClient extends BaseStorageDatabaseClient implements I }); } } +} + +class ApplicationStorageDatabaseClient extends BaseProfileAwareStorageDatabaseClient { + + constructor(channel: IChannel) { + super(channel, undefined); + } async close(): Promise { - // The global storage database is shared across all instances so + // The application storage database is shared across all instances so // we do not close it from the window. However we dispose the // listener for external changes because we no longer interested in it. @@ -93,12 +117,29 @@ class GlobalStorageDatabaseClient extends BaseStorageDatabaseClient implements I } } +class GlobalStorageDatabaseClient extends BaseProfileAwareStorageDatabaseClient { + + constructor(channel: IChannel, profile: IUserDataProfileDto) { + super(channel, profile); + } + + async close(): Promise { + + // The global storage database is shared across all instances of + // the same profile so we do not close it from the window. + // However we dispose the listener for external changes because + // we no longer interested in it. + + this.dispose(); + } +} + class WorkspaceStorageDatabaseClient extends BaseStorageDatabaseClient implements IStorageDatabase { readonly onDidChangeItemsExternal = Event.None; // unsupported for workspace storage because we only ever write from one window constructor(channel: IChannel, workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier) { - super(channel, workspace); + super(channel, undefined, workspace); } async close(): Promise { @@ -113,10 +154,19 @@ class WorkspaceStorageDatabaseClient extends BaseStorageDatabaseClient implement export class StorageDatabaseChannelClient extends Disposable { + private _applicationStorage: ApplicationStorageDatabaseClient | undefined = undefined; + get applicationStorage() { + if (!this._applicationStorage) { + this._applicationStorage = new ApplicationStorageDatabaseClient(this.channel); + } + + return this._applicationStorage; + } + private _globalStorage: GlobalStorageDatabaseClient | undefined = undefined; get globalStorage() { if (!this._globalStorage) { - this._globalStorage = new GlobalStorageDatabaseClient(this.channel); + this._globalStorage = new GlobalStorageDatabaseClient(this.channel, this.userDataProfileService.currentProfile); } return this._globalStorage; @@ -133,6 +183,7 @@ export class StorageDatabaseChannelClient extends Disposable { constructor( private channel: IChannel, + private userDataProfileService: IUserDataProfilesService, private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined ) { super(); diff --git a/src/vs/platform/storage/electron-main/storageIpc.ts b/src/vs/platform/storage/electron-main/storageIpc.ts index 9802b767d91b6..b0473d0d53d40 100644 --- a/src/vs/platform/storage/electron-main/storageIpc.ts +++ b/src/vs/platform/storage/electron-main/storageIpc.ts @@ -5,19 +5,22 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { ILogService } from 'vs/platform/log/common/log'; import { IBaseSerializableStorageRequest, ISerializableItemsChangeEvent, ISerializableUpdateRequest, Key, Value } from 'vs/platform/storage/common/storageIpc'; import { IStorageChangeEvent, IStorageMain } from 'vs/platform/storage/electron-main/storageMain'; import { IStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { reviveIdentifier, IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; export class StorageDatabaseChannel extends Disposable implements IServerChannel { private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100; - private readonly _onDidChangeGlobalStorage = this._register(new Emitter()); - private readonly onDidChangeGlobalStorage = this._onDidChangeGlobalStorage.event; + private readonly onDidChangeApplicationStorageEmitter = this._register(new Emitter()); + + private readonly mapProfileToOnDidChangeGlobalStorageEmitter = new Map>(); constructor( private logService: ILogService, @@ -25,16 +28,17 @@ export class StorageDatabaseChannel extends Disposable implements IServerChannel ) { super(); - this.registerGlobalStorageListeners(); + this.registerStorageChangeListeners(storageMainService.applicationStorage, this.onDidChangeApplicationStorageEmitter); } - //#region Global Storage Change Events + //#region Storage Change Events - private registerGlobalStorageListeners(): void { + private registerStorageChangeListeners(storage: IStorageMain, emitter: Emitter): void { - // Listen for changes in global storage to send to listeners + // Listen for changes in provided storage to send to listeners // that are listening. Use a debouncer to reduce IPC traffic. - this._register(Event.debounce(this.storageMainService.globalStorage.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => { + + this._register(Event.debounce(storage.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => { if (!prev) { prev = [cur]; } else { @@ -44,16 +48,16 @@ export class StorageDatabaseChannel extends Disposable implements IServerChannel return prev; }, StorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => { if (events.length) { - this._onDidChangeGlobalStorage.fire(this.serializeGlobalStorageEvents(events)); + emitter.fire(this.serializeStorageChangeEvents(events, storage)); } })); } - private serializeGlobalStorageEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent { + private serializeStorageChangeEvents(events: IStorageChangeEvent[], storage: IStorageMain): ISerializableItemsChangeEvent { const changed = new Map(); const deleted = new Set(); events.forEach(event => { - const existing = this.storageMainService.globalStorage.get(event.key); + const existing = storage.get(event.key); if (typeof existing === 'string') { changed.set(event.key, existing); } else { @@ -67,9 +71,26 @@ export class StorageDatabaseChannel extends Disposable implements IServerChannel }; } - listen(_: unknown, event: string): Event { + listen(_: unknown, event: string, arg: IBaseSerializableStorageRequest): Event { switch (event) { - case 'onDidChangeGlobalStorage': return this.onDidChangeGlobalStorage; + case 'onDidChangeStorage': { + const profile = arg.profile ? revive(arg.profile) : undefined; + + // Without profile: application scope + if (!profile) { + return this.onDidChangeApplicationStorageEmitter.event; + } + + // With profile: global scope for the profile + let globalStorageChangeEmitter = this.mapProfileToOnDidChangeGlobalStorageEmitter.get(profile.id); + if (!globalStorageChangeEmitter) { + globalStorageChangeEmitter = this._register(new Emitter()); + this.registerStorageChangeListeners(this.storageMainService.globalStorage(profile), globalStorageChangeEmitter); + this.mapProfileToOnDidChangeGlobalStorageEmitter.set(profile.id, globalStorageChangeEmitter); + } + + return globalStorageChangeEmitter.event; + } } throw new Error(`Event not found: ${event}`); @@ -78,10 +99,11 @@ export class StorageDatabaseChannel extends Disposable implements IServerChannel //#endregion async call(_: unknown, command: string, arg: IBaseSerializableStorageRequest): Promise { + const profile = arg.profile ? revive(arg.profile) : undefined; const workspace = reviveIdentifier(arg.workspace); // Get storage to be ready - const storage = await this.withStorageInitialized(workspace); + const storage = await this.withStorageInitialized(profile, workspace); // handle call switch (command) { @@ -110,13 +132,20 @@ export class StorageDatabaseChannel extends Disposable implements IServerChannel } } - private async withStorageInitialized(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): Promise { - const storage = workspace ? this.storageMainService.workspaceStorage(workspace) : this.storageMainService.globalStorage; + private async withStorageInitialized(profile: IUserDataProfile | undefined, workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): Promise { + let storage: IStorageMain; + if (workspace) { + storage = this.storageMainService.workspaceStorage(workspace); + } else if (profile) { + storage = this.storageMainService.globalStorage(profile); + } else { + storage = this.storageMainService.applicationStorage; + } try { await storage.init(); } catch (error) { - this.logService.error(`StorageIPC#init: Unable to init ${workspace ? 'workspace' : 'global'} storage due to ${error}`); + this.logService.error(`StorageIPC#init: Unable to init ${workspace ? 'workspace' : profile ? 'global' : 'application'} storage due to ${error}`); } return storage; diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index 7faf5ee69cda2..b4d9ab4256e9e 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -17,7 +17,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IFileService } from 'vs/platform/files/common/files'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IS_NEW_KEY } from 'vs/platform/storage/common/storage'; -import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { currentSessionDateStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; @@ -31,8 +31,8 @@ export interface IStorageMainOptions { } /** - * Provides access to global and workspace storage from the - * electron-main side that is the owner of all storage connections. + * Provides access to application, global and workspace storage from + * the electron-main side that is the owner of all storage connections. */ export interface IStorageMain extends IDisposable { @@ -255,22 +255,22 @@ abstract class BaseStorageMain extends Disposable implements IStorageMain { } } -export class GlobalStorageMain extends BaseStorageMain implements IStorageMain { +class BaseProfileAwareStorageMain extends BaseStorageMain { private static readonly STORAGE_NAME = 'state.vscdb'; get path(): string | undefined { if (!this.options.useInMemoryStorage) { - return join(this.userDataProfilesService.defaultProfile.globalStorageHome.fsPath, GlobalStorageMain.STORAGE_NAME); + return join(this.profile.globalStorageHome.fsPath, BaseProfileAwareStorageMain.STORAGE_NAME); } return undefined; } constructor( + private readonly profile: IUserDataProfile, private readonly options: IStorageMainOptions, logService: ILogService, - private readonly userDataProfilesService: IUserDataProfilesService, fileService: IFileService ) { super(logService, fileService); @@ -281,11 +281,35 @@ export class GlobalStorageMain extends BaseStorageMain implements IStorageMain { logging: this.createLoggingOptions() })); } +} + +export class GlobalStorageMain extends BaseProfileAwareStorageMain { + + constructor( + profile: IUserDataProfile, + options: IStorageMainOptions, + logService: ILogService, + fileService: IFileService + ) { + super(profile, options, logService, fileService); + } +} + +export class ApplicationStorageMain extends BaseProfileAwareStorageMain { + + constructor( + options: IStorageMainOptions, + userDataProfileService: IUserDataProfilesService, + logService: ILogService, + fileService: IFileService + ) { + super(userDataProfileService.defaultProfile, options, logService, fileService); + } protected override async doInit(storage: IStorage): Promise { await super.doInit(storage); - // Apply global telemetry values as part of the initialization + // Apply telemetry values as part of the application storage initialization this.updateTelemetryState(storage); } @@ -307,7 +331,7 @@ export class GlobalStorageMain extends BaseStorageMain implements IStorageMain { } } -export class WorkspaceStorageMain extends BaseStorageMain implements IStorageMain { +export class WorkspaceStorageMain extends BaseStorageMain { private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; private static readonly WORKSPACE_META_NAME = 'workspace.json'; diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 3c5e483fcb0bc..e1770742e7ee9 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -12,11 +12,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractStorageService, IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { GlobalStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain } from 'vs/platform/storage/electron-main/storageMain'; -import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { ApplicationStorageMain, GlobalStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain } from 'vs/platform/storage/electron-main/storageMain'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IAnyWorkspaceIdentifier, IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; -//#region Storage Main Service (intent: make global and workspace storage accessible to windows from main process) +//#region Storage Main Service (intent: make application, global and workspace storage accessible to windows from main process) export const IStorageMainService = createDecorator('storageMainService'); @@ -25,12 +25,22 @@ export interface IStorageMainService { readonly _serviceBrand: undefined; /** - * Provides access to the global storage shared across all windows. + * Provides access to the application storage shared across all + * windows and all profiles. * * Note: DO NOT use this for reading/writing from the main process! - * Rather use `IGlobalStorageMainService` for that purpose. + * Rather use `IApplicationStorageMainService` for that purpose. */ - readonly globalStorage: IStorageMain; + applicationStorage: IStorageMain; + + /** + * Provides access to the global storage shared across all windows + * for the provided profile. + * + * Note: DO NOT use this for reading/writing from the main process! + * This is currently not supported. + */ + globalStorage(profile: IUserDataProfile): IStorageMain; /** * Provides access to the workspace storage specific to a single window. @@ -67,15 +77,21 @@ export class StorageMainService extends Disposable implements IStorageMainServic private registerListeners(): void { - // Global Storage: Warmup when any window opens + // Application Storage: Warmup when any window opens (async () => { await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); - this.globalStorage.init(); + this.applicationStorage.init(); })(); - // Workspace Storage: Warmup when related window with workspace loads this._register(this.lifecycleMainService.onWillLoadWindow(e => { + + // Global Storage: Warmup when related window with profile loads + if (e.window.profile) { + this.globalStorage(e.window.profile).init(); + } + + // Workspace Storage: Warmup when related window with workspace loads if (e.workspace) { this.workspaceStorage(e.workspace).init(); } @@ -88,38 +104,84 @@ export class StorageMainService extends Disposable implements IStorageMainServic // Remember shutdown reason this.shutdownReason = e.reason; - // Global Storage - e.join(this.globalStorage.close()); + // Application Storage + e.join(this.applicationStorage.close()); + + // Global Storage(s) + for (const [, globalStorage] of this.mapProfileToStorage) { + e.join(globalStorage.close()); + } // Workspace Storage(s) - for (const [, storage] of this.mapWorkspaceToStorage) { - e.join(storage.close()); + for (const [, workspaceStorage] of this.mapWorkspaceToStorage) { + e.join(workspaceStorage.close()); } })); } - //#region Global Storage + //#region Application Storage - readonly globalStorage = this.createGlobalStorage(); + readonly applicationStorage = this.createApplicationStorage(); - private createGlobalStorage(): IStorageMain { - this.logService.trace(`StorageMainService: creating global storage`); + private createApplicationStorage(): IStorageMain { + this.logService.trace(`StorageMainService: creating application storage`); - const globalStorage = new GlobalStorageMain(this.getStorageOptions(), this.logService, this.userDataProfilesService, this.fileService); + const applicationStorage = new ApplicationStorageMain(this.getStorageOptions(), this.userDataProfilesService, this.logService, this.fileService); - once(globalStorage.onDidCloseStorage)(() => { - this.logService.trace(`StorageMainService: closed global storage`); + once(applicationStorage.onDidCloseStorage)(() => { + this.logService.trace(`StorageMainService: closed application storage`); }); + return applicationStorage; + } + + //#endregion + + //#region Global Storage + + private readonly mapProfileToStorage = new Map(); + + globalStorage(profile: IUserDataProfile): IStorageMain { + if (profile.isDefault) { + return this.applicationStorage; // for default profile, use application storage + } + + let globalStorage = this.mapProfileToStorage.get(profile.id); + if (!globalStorage) { + this.logService.trace(`StorageMainService: creating global storage (${profile.name})`); + + globalStorage = this.createGlobalStorage(profile); + this.mapProfileToStorage.set(profile.id, globalStorage); + + once(globalStorage.onDidCloseStorage)(() => { + this.logService.trace(`StorageMainService: closed global storage (${profile.name})`); + + this.mapProfileToStorage.delete(profile.id); + }); + } + return globalStorage; } + private createGlobalStorage(profile: IUserDataProfile): IStorageMain { + if (this.shutdownReason === ShutdownReason.KILL) { + + // Workaround for native crashes that we see when + // SQLite DBs are being created even after shutdown + // https://github.com/microsoft/vscode/issues/143186 + + return new InMemoryStorageMain(this.logService, this.fileService); + } + + return new GlobalStorageMain(profile, this.getStorageOptions(), this.logService, this.fileService); + } + //#endregion //#region Workspace Storage - private readonly mapWorkspaceToStorage = new Map(); + private readonly mapWorkspaceToStorage = new Map(); workspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain { let workspaceStorage = this.mapWorkspaceToStorage.get(workspace.id); @@ -158,15 +220,15 @@ export class StorageMainService extends Disposable implements IStorageMainServic //#endregion -//#region Global Main Storage Service (intent: use global storage from main process) +//#region Application Main Storage Service (intent: use application storage from main process) -export const IGlobalStorageMainService = createDecorator('globalStorageMainService'); +export const IApplicationStorageMainService = createDecorator('applicationStorageMainService'); /** * A specialized `IStorageService` interface that only allows - * access to the `StorageScope.GLOBAL` scope. + * access to the `StorageScope.APPLICATION` scope. */ -export interface IGlobalStorageMainService extends IStorageService { +export interface IApplicationStorageMainService extends IStorageService { /** * Important: unlike other storage services in the renderer, the @@ -174,38 +236,38 @@ export interface IGlobalStorageMainService extends IStorageService { * storage is being initialized while a window opens to reduce * pressure on startup. * - * As such, any client wanting to access global storage from the + * As such, any client wanting to access application storage from the * main process needs to wait for `whenReady`, otherwise there is * a chance that the service operates on an in-memory store that * is not backed by any persistent DB. */ readonly whenReady: Promise; - get(key: string, scope: StorageScope.GLOBAL, fallbackValue: string): string; - get(key: string, scope: StorageScope.GLOBAL, fallbackValue?: string): string | undefined; + get(key: string, scope: StorageScope.APPLICATION, fallbackValue: string): string; + get(key: string, scope: StorageScope.APPLICATION, fallbackValue?: string): string | undefined; - getBoolean(key: string, scope: StorageScope.GLOBAL, fallbackValue: boolean): boolean; - getBoolean(key: string, scope: StorageScope.GLOBAL, fallbackValue?: boolean): boolean | undefined; + getBoolean(key: string, scope: StorageScope.APPLICATION, fallbackValue: boolean): boolean; + getBoolean(key: string, scope: StorageScope.APPLICATION, fallbackValue?: boolean): boolean | undefined; - getNumber(key: string, scope: StorageScope.GLOBAL, fallbackValue: number): number; - getNumber(key: string, scope: StorageScope.GLOBAL, fallbackValue?: number): number | undefined; + getNumber(key: string, scope: StorageScope.APPLICATION, fallbackValue: number): number; + getNumber(key: string, scope: StorageScope.APPLICATION, fallbackValue?: number): number | undefined; - store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope.GLOBAL, target: StorageTarget): void; + store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope.APPLICATION, target: StorageTarget): void; - remove(key: string, scope: StorageScope.GLOBAL): void; + remove(key: string, scope: StorageScope.APPLICATION): void; - keys(scope: StorageScope.GLOBAL, target: StorageTarget): string[]; + keys(scope: StorageScope.APPLICATION, target: StorageTarget): string[]; migrate(toWorkspace: IAnyWorkspaceIdentifier): never; - isNew(scope: StorageScope.GLOBAL): boolean; + isNew(scope: StorageScope.APPLICATION): boolean; } -export class GlobalStorageMainService extends AbstractStorageService implements IGlobalStorageMainService { +export class ApplicationStorageMainService extends AbstractStorageService implements IApplicationStorageMainService { declare readonly _serviceBrand: undefined; - readonly whenReady = this.storageMainService.globalStorage.whenInit; + readonly whenReady = this.storageMainService.applicationStorage.whenInit; constructor( @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @@ -216,23 +278,26 @@ export class GlobalStorageMainService extends AbstractStorageService implements protected doInitialize(): Promise { - // global storage is being initialized as part - // of the first window opening, so we do not - // trigger it here but can join it - return this.storageMainService.globalStorage.whenInit; + // application storage is being initialized as part + // of the first window opening, so we do not trigger + // it here but can join it + return this.storageMainService.applicationStorage.whenInit; } protected getStorage(scope: StorageScope): IStorage | undefined { - switch (scope) { - case StorageScope.GLOBAL: - return this.storageMainService.globalStorage.storage; - case StorageScope.WORKSPACE: - return undefined; // unsupported from main process + if (scope === StorageScope.APPLICATION) { + return this.storageMainService.applicationStorage.storage; } + + return undefined; // any other scope is unsupported from main process } protected getLogDetails(scope: StorageScope): string | undefined { - return scope === StorageScope.GLOBAL ? this.userDataProfilesService.defaultProfile.globalStorageHome.fsPath : undefined; + if (scope === StorageScope.APPLICATION) { + return this.userDataProfilesService.defaultProfile.globalStorageHome.fsPath; + } + + return undefined; // any other scope is unsupported from main process } protected override shouldFlushWhenIdle(): boolean { diff --git a/src/vs/platform/storage/electron-sandbox/storageService.ts b/src/vs/platform/storage/electron-sandbox/storageService.ts index fd69b47c1410d..893dfbcb55229 100644 --- a/src/vs/platform/storage/electron-sandbox/storageService.ts +++ b/src/vs/platform/storage/electron-sandbox/storageService.ts @@ -16,7 +16,12 @@ import { IAnyWorkspaceIdentifier, IEmptyWorkspaceIdentifier, ISingleFolderWorksp export class NativeStorageService extends AbstractStorageService { + // Application Storage is readonly and shared across + // windows and profiles. + private readonly applicationStorage: IStorage; + // Global Storage is readonly and shared across windows + // under the same profile. private readonly globalStorage: IStorage; // Workspace Storage is scoped to a window but can change @@ -33,14 +38,35 @@ export class NativeStorageService extends AbstractStorageService { ) { super(); + this.applicationStorage = this.createApplicationStorage(); this.globalStorage = this.createGlobalStorage(); this.workspaceStorage = this.createWorkspaceStorage(workspace); } + private createApplicationStorage(): IStorage { + const storageDataBaseClient = new StorageDatabaseChannelClient(this.mainProcessService.getChannel('storage'), this.userDataProfilesService, undefined); + const applicationStorage = new Storage(storageDataBaseClient.applicationStorage); + + this._register(applicationStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.APPLICATION, key))); + + return applicationStorage; + } + private createGlobalStorage(): IStorage { - const storageDataBaseClient = new StorageDatabaseChannelClient(this.mainProcessService.getChannel('storage'), undefined); + let globalStorage: IStorage; + + if (this.userDataProfilesService.currentProfile.isDefault) { - const globalStorage = new Storage(storageDataBaseClient.globalStorage); + // If we are in default profile, the global storage is + // actually the same as application storage. As such we + // avoid creating the storage library a second time on + // the same DB. + + globalStorage = this.applicationStorage; + } else { + const storageDataBaseClient = new StorageDatabaseChannelClient(this.mainProcessService.getChannel('storage'), this.userDataProfilesService, undefined); + globalStorage = new Storage(storageDataBaseClient.globalStorage); + } this._register(globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key))); @@ -50,7 +76,7 @@ export class NativeStorageService extends AbstractStorageService { private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorage; private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): IStorage | undefined; private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): IStorage | undefined { - const storageDataBaseClient = new StorageDatabaseChannelClient(this.mainProcessService.getChannel('storage'), workspace); + const storageDataBaseClient = new StorageDatabaseChannelClient(this.mainProcessService.getChannel('storage'), this.userDataProfilesService, workspace); if (storageDataBaseClient.workspaceStorage) { const workspaceStorage = new Storage(storageDataBaseClient.workspaceStorage); @@ -71,17 +97,32 @@ export class NativeStorageService extends AbstractStorageService { // Init all storage locations await Promises.settled([ + this.applicationStorage.init(), this.globalStorage.init(), this.workspaceStorage?.init() ?? Promise.resolve() ]); } protected getStorage(scope: StorageScope): IStorage | undefined { - return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage; + switch (scope) { + case StorageScope.APPLICATION: + return this.applicationStorage; + case StorageScope.GLOBAL: + return this.globalStorage; + default: + return this.workspaceStorage; + } } protected getLogDetails(scope: StorageScope): string | undefined { - return scope === StorageScope.GLOBAL ? this.userDataProfilesService.defaultProfile.globalStorageHome.fsPath : this.workspaceStorageId ? `${joinPath(this.environmentService.workspaceStorageHome, this.workspaceStorageId, 'state.vscdb').fsPath}` : undefined; + switch (scope) { + case StorageScope.APPLICATION: + return this.userDataProfilesService.defaultProfile.globalStorageHome.fsPath; + case StorageScope.GLOBAL: + return this.userDataProfilesService.currentProfile.globalStorageHome.fsPath; + default: + return this.workspaceStorageId ? `${joinPath(this.environmentService.workspaceStorageHome, this.workspaceStorageId, 'state.vscdb').fsPath}` : undefined; + } } async close(): Promise { @@ -94,6 +135,7 @@ export class NativeStorageService extends AbstractStorageService { // Do it await Promises.settled([ + this.applicationStorage.close(), this.globalStorage.close(), this.workspaceStorage?.close() ?? Promise.resolve() ]); diff --git a/src/vs/platform/storage/test/browser/storageService.test.ts b/src/vs/platform/storage/test/browser/storageService.test.ts index cf51c013ef8a4..91bd5f6ebed67 100644 --- a/src/vs/platform/storage/test/browser/storageService.test.ts +++ b/src/vs/platform/storage/test/browser/storageService.test.ts @@ -6,15 +6,20 @@ import { strictEqual } from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; +import { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; import { Storage } from 'vs/base/parts/storage/common/storage'; +import { mock } from 'vs/base/test/common/mock'; import { flakySuite } from 'vs/base/test/common/testUtils'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { FileService } from 'vs/platform/files/common/fileService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { NullLogService } from 'vs/platform/log/common/log'; import { BrowserStorageService, IndexedDBStorageDatabase } from 'vs/platform/storage/browser/storageService'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { createSuite } from 'vs/platform/storage/test/common/storageService.test'; +import { IUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; async function createStorageService(): Promise<[DisposableStore, BrowserStorageService]> { const disposables = new DisposableStore(); @@ -25,7 +30,43 @@ async function createStorageService(): Promise<[DisposableStore, BrowserStorageS const userDataProvider = disposables.add(new InMemoryFileSystemProvider()); disposables.add(fileService.registerProvider(Schemas.vscodeUserData, userDataProvider)); - const storageService = disposables.add(new BrowserStorageService({ id: 'workspace-storage-test' }, logService)); + const profilesRoot = URI.file('/profiles').with({ scheme: Schemas.inMemory }); + + class EnvironmentServiceMock extends mock() { + override readonly userRoamingDataHome = profilesRoot; + } + + const inMemoryDefaultProfileRoot = joinPath(profilesRoot, 'default'); + const inMemoryDefaultProfile: IUserDataProfile = { + id: 'id', + name: 'inMemory', + isDefault: true, + location: inMemoryDefaultProfileRoot, + globalStorageHome: joinPath(inMemoryDefaultProfileRoot, 'globalStorageHome'), + settingsResource: joinPath(inMemoryDefaultProfileRoot, 'settingsResource'), + keybindingsResource: joinPath(inMemoryDefaultProfileRoot, 'keybindingsResource'), + tasksResource: joinPath(inMemoryDefaultProfileRoot, 'tasksResource'), + snippetsHome: joinPath(inMemoryDefaultProfileRoot, 'snippetsHome'), + extensionsResource: joinPath(inMemoryDefaultProfileRoot, 'extensionsResource') + }; + + const inMemoryExtraProfileRoot = joinPath(profilesRoot, 'extra'); + const inMemoryExtraProfile: IUserDataProfile = { + id: 'id', + name: 'inMemory', + isDefault: false, + location: inMemoryExtraProfileRoot, + globalStorageHome: joinPath(inMemoryExtraProfileRoot, 'globalStorageHome'), + settingsResource: joinPath(inMemoryExtraProfileRoot, 'settingsResource'), + keybindingsResource: joinPath(inMemoryExtraProfileRoot, 'keybindingsResource'), + tasksResource: joinPath(inMemoryExtraProfileRoot, 'tasksResource'), + snippetsHome: joinPath(inMemoryExtraProfileRoot, 'snippetsHome'), + extensionsResource: joinPath(inMemoryExtraProfileRoot, 'extensionsResource') + }; + + const userDataProfileService = new UserDataProfilesService(inMemoryDefaultProfile, inMemoryExtraProfile, new EnvironmentServiceMock(), fileService, new NullLogService()); + + const storageService = disposables.add(new BrowserStorageService({ id: 'workspace-storage-test' }, logService, userDataProfileService)); await storageService.initialize(); @@ -69,6 +110,8 @@ flakySuite('StorageService (browser specific)', () => { test('clear', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { + storageService.store('bar', 'foo', StorageScope.APPLICATION, StorageTarget.MACHINE); + storageService.store('bar', 3, StorageScope.APPLICATION, StorageTarget.USER); storageService.store('bar', 'foo', StorageScope.GLOBAL, StorageTarget.MACHINE); storageService.store('bar', 3, StorageScope.GLOBAL, StorageTarget.USER); storageService.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -76,7 +119,7 @@ flakySuite('StorageService (browser specific)', () => { await storageService.clear(); - for (const scope of [StorageScope.GLOBAL, StorageScope.WORKSPACE]) { + for (const scope of [StorageScope.APPLICATION, StorageScope.GLOBAL, StorageScope.WORKSPACE]) { for (const target of [StorageTarget.USER, StorageTarget.MACHINE]) { strictEqual(storageService.get('bar', scope), undefined); strictEqual(storageService.keys(scope, target).length, 0); diff --git a/src/vs/platform/storage/test/common/storageService.test.ts b/src/vs/platform/storage/test/common/storageService.test.ts index ea3af86c960fb..ab5ba71962ef5 100644 --- a/src/vs/platform/storage/test/common/storageService.test.ts +++ b/src/vs/platform/storage/test/common/storageService.test.ts @@ -18,6 +18,10 @@ export function createSuite(params: { setup: () => Pr return params.teardown(storageService); }); + test('Get Data, Integer, Boolean (application)', () => { + storeData(StorageScope.APPLICATION); + }); + test('Get Data, Integer, Boolean (global)', () => { storeData(StorageScope.GLOBAL); }); @@ -67,6 +71,10 @@ export function createSuite(params: { setup: () => Pr strictEqual(storageService.getBoolean('test.getBooleanDefault', scope, true), true); } + test('Remove Data (application)', () => { + removeData(StorageScope.APPLICATION); + }); + test('Remove Data (global)', () => { removeData(StorageScope.GLOBAL); }); @@ -97,14 +105,14 @@ export function createSuite(params: { setup: () => Pr storageService.onDidChangeValue(e => storageValueChangeEvent = e); // Empty - for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) { + for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL, StorageScope.APPLICATION]) { for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { strictEqual(storageService.keys(scope, target).length, 0); } } // Add values - for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) { + for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL, StorageScope.APPLICATION]) { for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { storageTargetEvent = Object.create(null); storageValueChangeEvent = Object.create(null); @@ -134,7 +142,7 @@ export function createSuite(params: { setup: () => Pr } // Remove values - for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) { + for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL, StorageScope.APPLICATION]) { for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { const keysLength = storageService.keys(scope, target).length; @@ -153,7 +161,7 @@ export function createSuite(params: { setup: () => Pr } // Remove all - for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) { + for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL, StorageScope.APPLICATION]) { for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { const keys = storageService.keys(scope, target); @@ -166,7 +174,7 @@ export function createSuite(params: { setup: () => Pr } // Adding undefined or null removes value - for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) { + for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL, StorageScope.APPLICATION]) { for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) { storageService.store('test.target1', 'value1', scope, target); strictEqual(storageService.keys(scope, target).length, 1); @@ -186,18 +194,20 @@ export function createSuite(params: { setup: () => Pr } // Target change - storageTargetEvent = undefined; - storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE); - ok(storageTargetEvent); - storageTargetEvent = undefined; - storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.USER); - ok(storageTargetEvent); - storageTargetEvent = undefined; - storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE); - ok(storageTargetEvent); - storageTargetEvent = undefined; - storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE); - ok(!storageTargetEvent); // no change in target + for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL, StorageScope.APPLICATION]) { + storageTargetEvent = undefined; + storageService.store('test.target5', 'value1', scope, StorageTarget.MACHINE); + ok(storageTargetEvent); + storageTargetEvent = undefined; + storageService.store('test.target5', 'value1', scope, StorageTarget.USER); + ok(storageTargetEvent); + storageTargetEvent = undefined; + storageService.store('test.target5', 'value1', scope, StorageTarget.MACHINE); + ok(storageTargetEvent); + storageTargetEvent = undefined; + storageService.store('test.target5', 'value1', scope, StorageTarget.MACHINE); + ok(!storageTargetEvent); // no change in target + } }); } diff --git a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts index ddd09f1eb1616..63292faaff45e 100644 --- a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts +++ b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -6,6 +6,9 @@ import { notStrictEqual, strictEqual } from 'assert'; import { Promises } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; +import { Schemas } from 'vs/base/common/network'; +import { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; @@ -15,17 +18,31 @@ import { ILifecycleMainService, LifecycleMainPhase, ShutdownEvent, ShutdownReaso import { NullLogService } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IS_NEW_KEY } from 'vs/platform/storage/common/storage'; +import { IS_NEW_KEY, StorageScope } from 'vs/platform/storage/common/storage'; import { IStorageChangeEvent, IStorageMain, IStorageMainOptions } from 'vs/platform/storage/electron-main/storageMain'; import { StorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { currentSessionDateStorageKey, firstSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; -import { UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ICodeWindow, UnloadReason } from 'vs/platform/window/electron-main/window'; suite('StorageMainService', function () { const productService: IProductService = { _serviceBrand: undefined, ...product }; + const inMemoryProfileRoot = URI.file('/location').with({ scheme: Schemas.inMemory }); + const inMemoryProfile: IUserDataProfile = { + id: 'id', + name: 'inMemory', + isDefault: false, + location: inMemoryProfileRoot, + globalStorageHome: joinPath(inMemoryProfileRoot, 'globalStorageHome'), + settingsResource: joinPath(inMemoryProfileRoot, 'settingsResource'), + keybindingsResource: joinPath(inMemoryProfileRoot, 'keybindingsResource'), + tasksResource: joinPath(inMemoryProfileRoot, 'tasksResource'), + snippetsHome: joinPath(inMemoryProfileRoot, 'snippetsHome'), + extensionsResource: joinPath(inMemoryProfileRoot, 'extensionsResource') + }; + class TestStorageMainService extends StorageMainService { protected override getStorageOptions(): IStorageMainOptions { @@ -74,10 +91,10 @@ suite('StorageMainService', function () { async when(phase: LifecycleMainPhase): Promise { } } - async function testStorage(storage: IStorageMain, isGlobal: boolean): Promise { + async function testStorage(storage: IStorageMain, scope: StorageScope): Promise { - // Telemetry: added after init - if (isGlobal) { + // Telemetry: added after init unless workspace/global scoped + if (scope === StorageScope.APPLICATION) { strictEqual(storage.items.size, 0); await storage.init(); strictEqual(typeof storage.get(firstSessionDateStorageKey), 'string'); @@ -131,54 +148,77 @@ suite('StorageMainService', function () { return new TestStorageMainService(new NullLogService(), environmentService, new UserDataProfilesService(undefined, undefined, environmentService, fileService, new NullLogService()), lifecycleMainService, fileService); } + test('basics (application)', function () { + const storageMainService = createStorageService(); + + return testStorage(storageMainService.applicationStorage, StorageScope.APPLICATION); + }); + test('basics (global)', function () { const storageMainService = createStorageService(); + const profile = inMemoryProfile; - return testStorage(storageMainService.globalStorage, true); + return testStorage(storageMainService.globalStorage(profile), StorageScope.GLOBAL); }); test('basics (workspace)', function () { const workspace = { id: generateUuid() }; const storageMainService = createStorageService(); - return testStorage(storageMainService.workspaceStorage(workspace), false); + return testStorage(storageMainService.workspaceStorage(workspace), StorageScope.WORKSPACE); }); test('storage closed onWillShutdown', async function () { const lifecycleMainService = new StorageTestLifecycleMainService(); - const workspace = { id: generateUuid() }; const storageMainService = createStorageService(lifecycleMainService); + const profile = inMemoryProfile; + const workspace = { id: generateUuid() }; + const workspaceStorage = storageMainService.workspaceStorage(workspace); let didCloseWorkspaceStorage = false; workspaceStorage.onDidCloseStorage(() => { didCloseWorkspaceStorage = true; }); - const globalStorage = storageMainService.globalStorage; + const globalStorage = storageMainService.globalStorage(profile); let didCloseGlobalStorage = false; globalStorage.onDidCloseStorage(() => { didCloseGlobalStorage = true; }); + const applicationStorage = storageMainService.applicationStorage; + let didCloseApplicationStorage = false; + applicationStorage.onDidCloseStorage(() => { + didCloseApplicationStorage = true; + }); + + strictEqual(applicationStorage, storageMainService.applicationStorage); // same instance as long as not closed + strictEqual(globalStorage, storageMainService.globalStorage(profile)); // same instance as long as not closed strictEqual(workspaceStorage, storageMainService.workspaceStorage(workspace)); // same instance as long as not closed + await applicationStorage.init(); await globalStorage.init(); await workspaceStorage.init(); await lifecycleMainService.fireOnWillShutdown(); + strictEqual(didCloseApplicationStorage, true); strictEqual(didCloseGlobalStorage, true); strictEqual(didCloseWorkspaceStorage, true); - const storage2 = storageMainService.workspaceStorage(workspace); - notStrictEqual(workspaceStorage, storage2); + const globalStorage2 = storageMainService.globalStorage(profile); + notStrictEqual(globalStorage, globalStorage2); - return storage2.close(); + const workspaceStorage2 = storageMainService.workspaceStorage(workspace); + notStrictEqual(workspaceStorage, workspaceStorage2); + + return workspaceStorage2.close(); }); test('storage closed before init works', async function () { const storageMainService = createStorageService(); + const profile = inMemoryProfile; const workspace = { id: generateUuid() }; const workspaceStorage = storageMainService.workspaceStorage(workspace); @@ -187,21 +227,30 @@ suite('StorageMainService', function () { didCloseWorkspaceStorage = true; }); - const globalStorage = storageMainService.globalStorage; + const globalStorage = storageMainService.globalStorage(profile); let didCloseGlobalStorage = false; globalStorage.onDidCloseStorage(() => { didCloseGlobalStorage = true; }); + const applicationStorage = storageMainService.applicationStorage; + let didCloseApplicationStorage = false; + applicationStorage.onDidCloseStorage(() => { + didCloseApplicationStorage = true; + }); + + await applicationStorage.close(); await globalStorage.close(); await workspaceStorage.close(); + strictEqual(didCloseApplicationStorage, true); strictEqual(didCloseGlobalStorage, true); strictEqual(didCloseWorkspaceStorage, true); }); test('storage closed before init awaits works', async function () { const storageMainService = createStorageService(); + const profile = inMemoryProfile; const workspace = { id: generateUuid() }; const workspaceStorage = storageMainService.workspaceStorage(workspace); @@ -210,18 +259,27 @@ suite('StorageMainService', function () { didCloseWorkspaceStorage = true; }); - const globalStorage = storageMainService.globalStorage; + const globalStorage = storageMainService.globalStorage(profile); let didCloseGlobalStorage = false; globalStorage.onDidCloseStorage(() => { didCloseGlobalStorage = true; }); + const applicationStorage = storageMainService.applicationStorage; + let didCloseApplicationStorage = false; + applicationStorage.onDidCloseStorage(() => { + didCloseApplicationStorage = true; + }); + + applicationStorage.init(); globalStorage.init(); workspaceStorage.init(); + await applicationStorage.close(); await globalStorage.close(); await workspaceStorage.close(); + strictEqual(didCloseApplicationStorage, true); strictEqual(didCloseGlobalStorage, true); strictEqual(didCloseWorkspaceStorage, true); }); diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index d10dd672e4df0..b916e7510be33 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -17,6 +17,7 @@ import { ILogService } from 'vs/platform/log/common/log'; export interface IUserDataProfile { readonly id: string; + readonly isDefault: boolean; readonly name: string; readonly location: URI; readonly globalStorageHome: URI; @@ -53,6 +54,7 @@ export interface IUserDataProfilesService { function reviveProfile(profile: IUserDataProfile, scheme: string): IUserDataProfile { return { id: profile.id, + isDefault: profile.isDefault, name: profile.name, location: URI.revive(profile.location).with({ scheme }), globalStorageHome: URI.revive(profile.globalStorageHome).with({ scheme }), @@ -93,9 +95,11 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } createProfile(name: string | undefined): IUserDataProfile { + const isDefault = !name || name === UserDataProfilesService.DEFAULT_PROFILE_NAME; const location = name && name !== UserDataProfilesService.DEFAULT_PROFILE_NAME ? joinPath(this.profilesHome, name) : this.environmentService.userRoamingDataHome; return { id: hash(location.toString()).toString(16), + isDefault, name: name ?? UserDataProfilesService.DEFAULT_PROFILE_NAME, location, globalStorageHome: joinPath(location, 'globalStorage'), diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index 1f76f5682019e..8ec5afe488fee 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -29,7 +29,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace'; -import { IGlobalStorageMainService, IStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { IApplicationStorageMainService, IStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; @@ -157,7 +157,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IPolicyService private readonly policyService: IPolicyService, @IFileService private readonly fileService: IFileService, - @IGlobalStorageMainService private readonly globalStorageMainService: IGlobalStorageMainService, + @IApplicationStorageMainService private readonly applicationStorageMainService: IApplicationStorageMainService, @IStorageMainService private readonly storageMainService: IStorageMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, @@ -521,7 +521,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private marketplaceHeadersPromise: Promise | undefined; private getMarketplaceHeaders(): Promise { if (!this.marketplaceHeadersPromise) { - this.marketplaceHeadersPromise = resolveMarketplaceHeaders(this.productService.version, this.productService, this.environmentMainService, this.configurationService, this.fileService, this.globalStorageMainService); + this.marketplaceHeadersPromise = resolveMarketplaceHeaders(this.productService.version, this.productService, this.environmentMainService, this.configurationService, this.fileService, this.applicationStorageMainService); } return this.marketplaceHeadersPromise; diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index c4247ff6b0e1d..0c9f5ee790c04 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -19,7 +19,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IGlobalStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { IApplicationStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { ICodeWindow } from 'vs/platform/window/electron-main/window'; import { IRecent, IRecentFile, IRecentFolder, IRecentlyOpened, IRecentWorkspace, isRecentFile, isRecentFolder, isRecentWorkspace, restoreRecentlyOpened, toStoreData } from 'vs/platform/workspaces/common/workspaces'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, WORKSPACE_EXTENSION } from 'vs/platform/workspace/common/workspace'; @@ -54,7 +54,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa @ILogService private readonly logService: ILogService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, - @IGlobalStorageMainService private readonly globalStorageMainService: IGlobalStorageMainService + @IApplicationStorageMainService private readonly applicationStorageMainService: IApplicationStorageMainService ) { super(); @@ -217,13 +217,13 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa private async getRecentlyOpenedFromStorage(): Promise { - // Wait for global storage to be ready - await this.globalStorageMainService.whenReady; + // Wait for application storage to be ready + await this.applicationStorageMainService.whenReady; let storedRecentlyOpened: object | undefined = undefined; // First try with storage service - const storedRecentlyOpenedRaw = this.globalStorageMainService.get(WorkspacesHistoryMainService.RECENTLY_OPENED_STORAGE_KEY, StorageScope.GLOBAL); + const storedRecentlyOpenedRaw = this.applicationStorageMainService.get(WorkspacesHistoryMainService.RECENTLY_OPENED_STORAGE_KEY, StorageScope.APPLICATION); if (typeof storedRecentlyOpenedRaw === 'string') { try { storedRecentlyOpened = JSON.parse(storedRecentlyOpenedRaw); @@ -237,11 +237,11 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa private async saveRecentlyOpened(recent: IRecentlyOpened): Promise { - // Wait for global storage to be ready - await this.globalStorageMainService.whenReady; + // Wait for application storage to be ready + await this.applicationStorageMainService.whenReady; - // Store in global storage (but do not sync since this is mainly local paths) - this.globalStorageMainService.store(WorkspacesHistoryMainService.RECENTLY_OPENED_STORAGE_KEY, JSON.stringify(toStoreData(recent)), StorageScope.GLOBAL, StorageTarget.MACHINE); + // Store in application storage (but do not sync since this is mainly local paths) + this.applicationStorageMainService.store(WorkspacesHistoryMainService.RECENTLY_OPENED_STORAGE_KEY, JSON.stringify(toStoreData(recent)), StorageScope.APPLICATION, StorageTarget.MACHINE); } private location(recent: IRecent): URI { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 0a1507fe9fc59..5ebd48271c08e 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -254,7 +254,7 @@ export class BrowserMain extends Disposable { return service; }), - this.createStorageService(payload, logService).then(service => { + this.createStorageService(payload, logService, userDataProfilesService).then(service => { // Storage serviceCollection.set(IStorageService, service); @@ -426,8 +426,8 @@ export class BrowserMain extends Disposable { }); } - private async createStorageService(payload: IAnyWorkspaceIdentifier, logService: ILogService): Promise { - const storageService = new BrowserStorageService(payload, logService); + private async createStorageService(payload: IAnyWorkspaceIdentifier, logService: ILogService, userDataProfilesService: IUserDataProfilesService): Promise { + const storageService = new BrowserStorageService(payload, logService, userDataProfilesService); try { await storageService.initialize(); diff --git a/src/vs/workbench/common/memento.ts b/src/vs/workbench/common/memento.ts index 2d52437793a7c..1c506756d1ad1 100644 --- a/src/vs/workbench/common/memento.ts +++ b/src/vs/workbench/common/memento.ts @@ -11,6 +11,7 @@ export type MementoObject = { [key: string]: any }; export class Memento { + private static readonly applicationMementos = new Map(); private static readonly globalMementos = new Map(); private static readonly workspaceMementos = new Map(); @@ -23,53 +24,60 @@ export class Memento { } getMemento(scope: StorageScope, target: StorageTarget): MementoObject { + switch (scope) { - // Scope by Workspace - if (scope === StorageScope.WORKSPACE) { - let workspaceMemento = Memento.workspaceMementos.get(this.id); - if (!workspaceMemento) { - workspaceMemento = new ScopedMemento(this.id, scope, target, this.storageService); - Memento.workspaceMementos.set(this.id, workspaceMemento); - } + // Scope by Workspace + case StorageScope.WORKSPACE: { + let workspaceMemento = Memento.workspaceMementos.get(this.id); + if (!workspaceMemento) { + workspaceMemento = new ScopedMemento(this.id, scope, target, this.storageService); + Memento.workspaceMementos.set(this.id, workspaceMemento); + } - return workspaceMemento.getMemento(); - } + return workspaceMemento.getMemento(); + } - // Scope Global - let globalMemento = Memento.globalMementos.get(this.id); - if (!globalMemento) { - globalMemento = new ScopedMemento(this.id, scope, target, this.storageService); - Memento.globalMementos.set(this.id, globalMemento); - } + // Scope Global + case StorageScope.GLOBAL: { + let globalMemento = Memento.globalMementos.get(this.id); + if (!globalMemento) { + globalMemento = new ScopedMemento(this.id, scope, target, this.storageService); + Memento.globalMementos.set(this.id, globalMemento); + } - return globalMemento.getMemento(); - } + return globalMemento.getMemento(); + } - saveMemento(): void { + // Scope Application + case StorageScope.APPLICATION: { + let applicationMemento = Memento.applicationMementos.get(this.id); + if (!applicationMemento) { + applicationMemento = new ScopedMemento(this.id, scope, target, this.storageService); + Memento.applicationMementos.set(this.id, applicationMemento); + } - // Workspace - const workspaceMemento = Memento.workspaceMementos.get(this.id); - if (workspaceMemento) { - workspaceMemento.save(); + return applicationMemento.getMemento(); + } } + } - // Global - const globalMemento = Memento.globalMementos.get(this.id); - if (globalMemento) { - globalMemento.save(); - } + saveMemento(): void { + Memento.workspaceMementos.get(this.id)?.save(); + Memento.globalMementos.get(this.id)?.save(); + Memento.applicationMementos.get(this.id)?.save(); } static clear(scope: StorageScope): void { - - // Workspace - if (scope === StorageScope.WORKSPACE) { - Memento.workspaceMementos.clear(); - } - - // Global - if (scope === StorageScope.GLOBAL) { - Memento.globalMementos.clear(); + switch (scope) { + case StorageScope.WORKSPACE: + Memento.workspaceMementos.clear(); + break; + case StorageScope.GLOBAL: + Memento.globalMementos.clear(); + break; + case StorageScope.APPLICATION: + Memento.applicationMementos.clear(); + break; } } } diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index 07741ba7717e4..c66a550ef6380 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -483,7 +483,7 @@ export class ExperimentService extends Disposable implements IExperimentService return Promise.resolve(ExperimentState.NoRun); } - const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL); + const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION); if ((condition.newUser === true && !isNewUser) || (condition.newUser === false && isNewUser)) { return Promise.resolve(ExperimentState.NoRun); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 5fac78668bfe1..0c1cd126cccb2 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -815,7 +815,7 @@ export class GettingStartedPage extends EditorPane { this.buildTelemetryFooter(telemetryNotice); footer.appendChild(telemetryNotice); } else if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory) { - const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL) || new Date().toUTCString(); + const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION) || new Date().toUTCString(); const daysSinceFirstSession = ((+new Date()) - (+new Date(firstSessionDateString))) / 1000 / 60 / 60 / 24; const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index'; diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index ea3df42e4f9e6..f618b5e9ce046 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter, INeverShowAgainOptions } from 'vs/platform/notification/common/notification'; import { NotificationsModel, ChoiceAction, NotificationChangeType } from 'vs/workbench/common/notifications'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; @@ -98,7 +98,7 @@ export class NotificationService extends Disposable implements INotificationServ // Handle neverShowAgain option accordingly if (notification.neverShowAgain) { - const scope = notification.neverShowAgain.scope === NeverShowAgainScope.WORKSPACE ? StorageScope.WORKSPACE : StorageScope.GLOBAL; + const scope = this.toStorageScope(notification.neverShowAgain); const id = notification.neverShowAgain.id; // If the user already picked to not show the notification @@ -142,12 +142,25 @@ export class NotificationService extends Disposable implements INotificationServ return handle; } + private toStorageScope(options: INeverShowAgainOptions): StorageScope { + switch (options.scope) { + case NeverShowAgainScope.APPLICATION: + return StorageScope.APPLICATION; + case NeverShowAgainScope.GLOBAL: + return StorageScope.GLOBAL; + case NeverShowAgainScope.WORKSPACE: + return StorageScope.WORKSPACE; + default: + return StorageScope.APPLICATION; + } + } + prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle { const toDispose = new DisposableStore(); // Handle neverShowAgain option accordingly if (options?.neverShowAgain) { - const scope = options.neverShowAgain.scope === NeverShowAgainScope.WORKSPACE ? StorageScope.WORKSPACE : StorageScope.GLOBAL; + const scope = this.toStorageScope(options.neverShowAgain); const id = options.neverShowAgain.id; // If the user already picked to not show the notification diff --git a/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts index 69116d9a45823..16bce794a23e9 100644 --- a/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts @@ -30,15 +30,15 @@ export async function resolveWorkbenchCommonProperties( resolveAdditionalProperties?: () => { [key: string]: any } ): Promise<{ [name: string]: string | undefined }> { const result: { [name: string]: string | undefined } = Object.create(null); - const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL)!; - const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL)!; + const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; + const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; let machineId: string | undefined; if (!removeMachineId) { - machineId = storageService.get(machineIdKey, StorageScope.GLOBAL); + machineId = storageService.get(machineIdKey, StorageScope.APPLICATION); if (!machineId) { machineId = uuid.generateUuid(); - storageService.store(machineIdKey, machineId, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(machineIdKey, machineId, StorageScope.APPLICATION, StorageTarget.MACHINE); } } else { machineId = `Redacted-${productIdentifier ?? 'web'}`; diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts index ff36a5231e62a..9d5b4282308e3 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts @@ -23,8 +23,8 @@ export async function resolveWorkbenchCommonProperties( remoteAuthority?: string ): Promise<{ [name: string]: string | boolean | undefined }> { const result = await resolveCommonProperties(fileService, release, hostname, process.arch, commit, version, machineId, msftInternalDomains, installSourcePath); - const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL)!; - const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL)!; + const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; + const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; // __GDPR__COMMON__ "common.version.shell" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['common.version.shell'] = process.versions['electron']; diff --git a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts index ac59653494029..8ae51addd3e5d 100644 --- a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts @@ -72,7 +72,7 @@ suite('Telemetry - common properties', function () { test('lastSessionDate when available', async function () { - testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.GLOBAL, StorageTarget.MACHINE); + testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.APPLICATION, StorageTarget.MACHINE); const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); assert.ok('common.lastSessionDate' in props); // conditional, see below diff --git a/src/vs/workbench/test/common/memento.test.ts b/src/vs/workbench/test/common/memento.test.ts index 2f84c262933fc..3ba00fa5c6d8a 100644 --- a/src/vs/workbench/test/common/memento.test.ts +++ b/src/vs/workbench/test/common/memento.test.ts @@ -9,11 +9,11 @@ import { Memento } from 'vs/workbench/common/memento'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Memento', () => { - const context: StorageScope | undefined = undefined; let storage: IStorageService; setup(() => { storage = new TestStorageService(); + Memento.clear(StorageScope.APPLICATION); Memento.clear(StorageScope.GLOBAL); Memento.clear(StorageScope.WORKSPACE); }); @@ -22,8 +22,14 @@ suite('Memento', () => { const myMemento = new Memento('memento.test', storage); // Global - let memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + let memento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); memento.foo = [1, 2, 3]; + let applicationMemento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); + assert.deepStrictEqual(applicationMemento, memento); + + // Global + memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + memento.foo = [4, 5, 6]; let globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(globalMemento, memento); @@ -34,9 +40,15 @@ suite('Memento', () => { myMemento.saveMemento(); + // Application + memento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); + assert.deepStrictEqual(memento, { foo: [1, 2, 3] }); + applicationMemento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); + assert.deepStrictEqual(applicationMemento, memento); + // Global memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - assert.deepStrictEqual(memento, { foo: [1, 2, 3] }); + assert.deepStrictEqual(memento, { foo: [4, 5, 6] }); globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(globalMemento, memento); @@ -45,12 +57,16 @@ suite('Memento', () => { assert.deepStrictEqual(memento, { foo: 'Hello World' }); // Assert the Mementos are stored properly in storage - assert.deepStrictEqual(JSON.parse(storage.get('memento/memento.test', StorageScope.GLOBAL)!), { foo: [1, 2, 3] }); - + assert.deepStrictEqual(JSON.parse(storage.get('memento/memento.test', StorageScope.APPLICATION)!), { foo: [1, 2, 3] }); + assert.deepStrictEqual(JSON.parse(storage.get('memento/memento.test', StorageScope.GLOBAL)!), { foo: [4, 5, 6] }); assert.deepStrictEqual(JSON.parse(storage.get('memento/memento.test', StorageScope.WORKSPACE)!), { foo: 'Hello World' }); + // Delete Application + memento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); + delete memento.foo; + // Delete Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); delete memento.foo; // Delete Workspace @@ -59,8 +75,12 @@ suite('Memento', () => { myMemento.saveMemento(); + // Application + memento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); + assert.deepStrictEqual(memento, {}); + // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(memento, {}); // Workspace @@ -68,8 +88,8 @@ suite('Memento', () => { assert.deepStrictEqual(memento, {}); // Assert the Mementos are also removed from storage + assert.strictEqual(storage.get('memento/memento.test', StorageScope.APPLICATION, null!), null); assert.strictEqual(storage.get('memento/memento.test', StorageScope.GLOBAL, null!), null); - assert.strictEqual(storage.get('memento/memento.test', StorageScope.WORKSPACE, null!), null); }); @@ -77,7 +97,7 @@ suite('Memento', () => { const myMemento = new Memento('memento.test', storage); // Global - let memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + let memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); memento.foo = [1, 2, 3]; // Workspace @@ -88,7 +108,7 @@ suite('Memento', () => { myMemento.saveMemento(); // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: [1, 2, 3] }); let globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(globalMemento, memento); @@ -98,7 +118,7 @@ suite('Memento', () => { assert.deepStrictEqual(memento, { foo: 'Hello World' }); // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); memento.foo = [4, 5, 6]; // Workspace @@ -109,7 +129,7 @@ suite('Memento', () => { myMemento.saveMemento(); // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: [4, 5, 6] }); globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(globalMemento, memento); @@ -119,7 +139,7 @@ suite('Memento', () => { assert.deepStrictEqual(memento, { foo: 'World Hello' }); // Delete Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); delete memento.foo; // Delete Workspace @@ -129,7 +149,7 @@ suite('Memento', () => { myMemento.saveMemento(); // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(memento, {}); // Workspace @@ -142,10 +162,10 @@ suite('Memento', () => { const myMemento2 = new Memento('memento.test', storage); // Global - let memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + let memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); memento.foo = [1, 2, 3]; - memento = myMemento2.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento2.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); memento.bar = [1, 2, 3]; // Workspace @@ -161,12 +181,12 @@ suite('Memento', () => { myMemento2.saveMemento(); // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: [1, 2, 3], bar: [1, 2, 3] }); let globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(globalMemento, memento); - memento = myMemento2.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento2.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: [1, 2, 3], bar: [1, 2, 3] }); globalMemento = myMemento2.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); assert.deepStrictEqual(globalMemento, memento); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 1738a1903dc90..79d2fefb506d5 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -58,6 +58,7 @@ const homeDir = homedir(); const NULL_PROFILE = { name: '', id: '', + isDefault: false, location: URI.file(homeDir), settingsResource: joinPath(URI.file(homeDir), 'settings.json'), globalStorageHome: joinPath(URI.file(homeDir), 'globalStorage'),