chore(vscode): update to 1.54.2

This commit is contained in:
Joe Previte
2021-03-11 10:27:10 -07:00
1459 changed files with 53404 additions and 51004 deletions

View File

@@ -3,21 +3,22 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { StorageScope, logStorage, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { StorageScope, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async';
import { Promises } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
export class BrowserStorageService extends AbstractStorageService {
private static BROWSER_DEFAULT_FLUSH_INTERVAL = 5 * 1000; // every 5s because async operations are not permitted on shutdown
private globalStorage: IStorage | undefined;
private workspaceStorage: IStorage | undefined;
@@ -27,38 +28,26 @@ export class BrowserStorageService extends AbstractStorageService {
private globalStorageFile: URI | undefined;
private workspaceStorageFile: URI | undefined;
private initializePromise: Promise<void> | undefined;
private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 5000 /* every 5s */));
private runWhenIdleDisposable: IDisposable | undefined = undefined;
get hasPendingUpdate(): boolean {
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
}
constructor(
private readonly payload: IWorkspaceInitializationPayload,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService
) {
super();
super({ flushInterval: BrowserStorageService.BROWSER_DEFAULT_FLUSH_INTERVAL });
}
initialize(payload: IWorkspaceInitializationPayload): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = this.doInitialize(payload);
}
return this.initializePromise;
}
private async doInitialize(payload: IWorkspaceInitializationPayload): Promise<void> {
protected async doInitialize(): Promise<void> {
// Ensure state folder exists
const stateRoot = joinPath(this.environmentService.userRoamingDataHome, 'state');
await this.fileService.createFolder(stateRoot);
// Workspace Storage
this.workspaceStorageFile = joinPath(stateRoot, `${payload.id}.json`);
this.workspaceStorageFile = joinPath(stateRoot, `${this.payload.id}.json`);
this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService));
this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase));
@@ -71,7 +60,7 @@ export class BrowserStorageService extends AbstractStorageService {
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
// Init both
await Promise.all([
await Promises.settled([
this.workspaceStorage.init(),
this.globalStorage.init()
]);
@@ -91,91 +80,36 @@ export class BrowserStorageService extends AbstractStorageService {
} else if (firstWorkspaceOpen) {
this.workspaceStorage.set(IS_NEW_KEY, false);
}
// In the browser we do not have support for long running unload sequences. As such,
// we cannot ask for saving state in that moment, because that would result in a
// long running operation.
// Instead, periodically ask customers to save save. The library will be clever enough
// to only save state that has actually changed.
this.periodicFlushScheduler.schedule();
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope): string | undefined;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
return this.getStorage(scope).get(key, fallbackValue);
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope): boolean | undefined;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
return this.getStorage(scope).getBoolean(key, fallbackValue);
}
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope): number | undefined;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
return this.getStorage(scope).getNumber(key, fallbackValue);
}
<<<<<<< HEAD
protected doStore(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
return this.getStorage(scope).set(key, value);
=======
protected getStorage(scope: StorageScope): IStorage | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
>>>>>>> e8cd17a97d8c58fffcbac05394b3ee2b3c72d384
}
protected doRemove(key: string, scope: StorageScope): void {
this.getStorage(scope).delete(key);
}
private getStorage(scope: StorageScope): IStorage {
return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage);
}
async logStorage(): Promise<void> {
const [globalStorage, workspaceStorage, globalStorageFile, workspaceStorageFile] = assertAllDefined(this.globalStorage, this.workspaceStorage, this.globalStorageFile, this.workspaceStorageFile);
const result = await Promise.all([
globalStorage.items,
workspaceStorage.items
]);
return logStorage(result[0], result[1], globalStorageFile.toString(), workspaceStorageFile.toString());
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorageFile?.toString() : this.workspaceStorageFile?.toString();
}
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
throw new Error('Migrating storage is currently unsupported in Web');
}
protected async doFlush(): Promise<void> {
await Promise.all([
this.getStorage(StorageScope.GLOBAL).whenFlushed(),
this.getStorage(StorageScope.WORKSPACE).whenFlushed()
]);
}
private doFlushWhenIdle(): void {
// Dispose any previous idle runner
dispose(this.runWhenIdleDisposable);
// Run when idle
this.runWhenIdleDisposable = runWhenIdle(() => {
// this event will potentially cause new state to be stored
// since new state will only be created while the document
// has focus, one optimization is to not run this when the
// document has no focus, assuming that state has not changed
//
// another optimization is to not collect more state if we
// have a pending update already running which indicates
// that the connection is either slow or disconnected and
// thus unhealthy.
if (document.hasFocus() && !this.hasPendingUpdate) {
this.flush();
}
// repeat
this.periodicFlushScheduler.schedule();
});
protected shouldFlushWhenIdle(): boolean {
// this flush() will potentially cause new state to be stored
// since new state will only be created while the document
// has focus, one optimization is to not run this when the
// document has no focus, assuming that state has not changed
//
// another optimization is to not collect more state if we
// have a pending update already running which indicates
// that the connection is either slow or disconnected and
// thus unhealthy.
return document.hasFocus() && !this.hasPendingUpdate;
}
close(): void {
@@ -189,13 +123,6 @@ export class BrowserStorageService extends AbstractStorageService {
// get triggered in this phase.
this.dispose();
}
dispose(): void {
dispose(this.runWhenIdleDisposable);
this.runWhenIdleDisposable = undefined;
super.dispose();
}
}
export class FileStorageDatabase extends Disposable implements IStorageDatabase {

View File

@@ -5,9 +5,11 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter, PauseableEmitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, dispose, MutableDisposable } from 'vs/base/common/lifecycle';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { InMemoryStorageDatabase, IStorage, Storage } from 'vs/base/parts/storage/common/storage';
import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
export const IS_NEW_KEY = '__$__isNewStorageMarker';
const TARGET_KEY = '__$__targetStorageMarker';
@@ -218,10 +220,16 @@ interface IKeyTargets {
[key: string]: StorageTarget
}
export interface IStorageServiceOptions {
flushInterval: number;
}
export abstract class AbstractStorageService extends Disposable implements IStorageService {
declare readonly _serviceBrand: undefined;
private static DEFAULT_FLUSH_INTERVAL = 60 * 1000; // every minute
private readonly _onDidChangeValue = this._register(new PauseableEmitter<IStorageValueChangeEvent>());
readonly onDidChangeValue = this._onDidChangeValue.event;
@@ -231,6 +239,56 @@ export abstract class AbstractStorageService extends Disposable implements IStor
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
readonly onWillSaveState = this._onWillSaveState.event;
private initializationPromise: Promise<void> | undefined;
private readonly flushWhenIdleScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), this.options.flushInterval));
private readonly runFlushWhenIdle = this._register(new MutableDisposable());
constructor(private options: IStorageServiceOptions = { flushInterval: AbstractStorageService.DEFAULT_FLUSH_INTERVAL }) {
super();
}
private doFlushWhenIdle(): void {
this.runFlushWhenIdle.value = runWhenIdle(() => {
if (this.shouldFlushWhenIdle()) {
this.flush();
}
// repeat
this.flushWhenIdleScheduler.schedule();
});
}
protected shouldFlushWhenIdle(): boolean {
return true;
}
protected stopFlushWhenIdle(): void {
dispose([this.runFlushWhenIdle, this.flushWhenIdleScheduler]);
}
initialize(): Promise<void> {
if (!this.initializationPromise) {
this.initializationPromise = (async () => {
// Ask subclasses to initialize storage
await this.doInitialize();
// On some OS we do not get enough time to persist state on shutdown (e.g. when
// Windows restarts after applying updates). In other cases, VSCode might crash,
// so we periodically save state to reduce the chance of loosing any state.
// In the browser we do not have support for long running unload sequences. As such,
// we cannot ask for saving state in that moment, because that would result in a
// long running operation.
// Instead, periodically ask customers to save save. The library will be clever enough
// to only save state that has actually changed.
this.flushWhenIdleScheduler.schedule();
})();
}
return this.initializationPromise;
}
protected emitDidChangeValue(scope: StorageScope, key: string): void {
// Specially handle `TARGET_KEY`
@@ -257,6 +315,24 @@ export abstract class AbstractStorageService extends Disposable implements IStor
this._onWillSaveState.fire({ reason });
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope): string | undefined;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
return this.getStorage(scope)?.get(key, fallbackValue);
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope): boolean | undefined;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
return this.getStorage(scope)?.getBoolean(key, fallbackValue);
}
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope): number | undefined;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
return this.getStorage(scope)?.getNumber(key, fallbackValue);
}
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): void {
// We remove the key for undefined/null values
@@ -272,7 +348,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
this.updateKeyTarget(key, scope, target);
// Store actual value
this.doStore(key, value, scope);
this.getStorage(scope)?.set(key, value);
});
}
@@ -285,7 +361,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
this.updateKeyTarget(key, scope, undefined);
// Remove actual key
this.doRemove(key, scope);
this.getStorage(scope)?.delete(key);
});
}
@@ -326,7 +402,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
if (typeof target === 'number') {
if (keyTargets[key] !== target) {
keyTargets[key] = target;
this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope);
this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets));
}
}
@@ -334,7 +410,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
else {
if (typeof keyTargets[key] === 'number') {
delete keyTargets[key];
this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope);
this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets));
}
}
}
@@ -378,118 +454,70 @@ export abstract class AbstractStorageService extends Disposable implements IStor
return this.getBoolean(IS_NEW_KEY, scope) === true;
}
flush(): Promise<void> {
async flush(): Promise<void> {
// Signal event to collect changes
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
// Await flush
return this.doFlush();
await Promises.settled([
this.getStorage(StorageScope.GLOBAL)?.whenFlushed() ?? Promise.resolve(),
this.getStorage(StorageScope.WORKSPACE)?.whenFlushed() ?? Promise.resolve()
]);
}
async logStorage(): Promise<void> {
const globalItems = this.getStorage(StorageScope.GLOBAL)?.items ?? new Map<string, string>();
const workspaceItems = this.getStorage(StorageScope.WORKSPACE)?.items ?? new Map<string, string>();
return logStorage(
globalItems,
workspaceItems,
this.getLogDetails(StorageScope.GLOBAL) ?? '',
this.getLogDetails(StorageScope.WORKSPACE) ?? ''
);
}
// --- abstract
abstract get(key: string, scope: StorageScope, fallbackValue: string): string;
abstract get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;
abstract getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
abstract getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;
abstract getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
abstract getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
<<<<<<< HEAD
protected abstract doStore(key: string, value: string | boolean | number, scope: StorageScope): Promise<void> | void;
=======
protected abstract doInitialize(): Promise<void>;
>>>>>>> e8cd17a97d8c58fffcbac05394b3ee2b3c72d384
protected abstract doRemove(key: string, scope: StorageScope): void;
protected abstract getStorage(scope: StorageScope): IStorage | undefined;
protected abstract doFlush(): Promise<void>;
protected abstract getLogDetails(scope: StorageScope): string | undefined;
abstract migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void>;
abstract logStorage(): void;
}
export class InMemoryStorageService extends AbstractStorageService {
private readonly globalCache = new Map<string, string>();
private readonly workspaceCache = new Map<string, string>();
private globalStorage = new Storage(new InMemoryStorageDatabase());
private workspaceStorage = new Storage(new InMemoryStorageDatabase());
private getCache(scope: StorageScope): Map<string, string> {
return scope === StorageScope.GLOBAL ? this.globalCache : this.workspaceCache;
constructor() {
super();
this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)));
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
const value = this.getCache(scope).get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value;
protected getStorage(scope: StorageScope): IStorage {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
const value = this.getCache(scope).get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value === 'true';
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? 'inMemory (global)' : 'inMemory (workspace)';
}
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
const value = this.getCache(scope).get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return parseInt(value, 10);
}
protected doStore(key: string, value: string | boolean | number, scope: StorageScope): void {
// Otherwise, convert to String and store
const valueStr = String(value);
// Return early if value already set
const currentValue = this.getCache(scope).get(key);
if (currentValue === valueStr) {
return;
}
// Update in cache
this.getCache(scope).set(key, valueStr);
// Events
this.emitDidChangeValue(scope, key);
}
protected doRemove(key: string, scope: StorageScope): void {
const wasDeleted = this.getCache(scope).delete(key);
if (!wasDeleted) {
return; // Return early if value already deleted
}
// Events
this.emitDidChangeValue(scope, key);
}
logStorage(): void {
logStorage(this.globalCache, this.workspaceCache, 'inMemory', 'inMemory');
}
protected async doInitialize(): Promise<void> { }
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
// not supported
}
async doFlush(): Promise<void> { }
async close(): Promise<void> { }
}
export async function logStorage(global: Map<string, string>, workspace: Map<string, string>, globalPath: string, workspacePath: string): Promise<void> {

View File

@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { IEmptyWorkspaceIdentifier, ISerializedSingleFolderWorkspaceIdentifier, ISerializedWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
export type Key = string;
export type Value = string;
export type Item = [Key, Value];
export interface IBaseSerializableStorageRequest {
readonly workspace: ISerializedWorkspaceIdentifier | ISerializedSingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined
}
export interface ISerializableUpdateRequest extends IBaseSerializableStorageRequest {
insert?: Item[];
delete?: Key[];
}
export interface ISerializableItemsChangeEvent {
readonly changed?: Item[];
readonly deleted?: Key[];
}
abstract class BaseStorageDatabaseClient extends Disposable implements IStorageDatabase {
abstract readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
constructor(protected channel: IChannel, protected workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined) {
super();
}
async getItems(): Promise<Map<string, string>> {
const serializableRequest: IBaseSerializableStorageRequest = { workspace: this.workspace };
const items: Item[] = await this.channel.call('getItems', serializableRequest);
return new Map(items);
}
updateItems(request: IUpdateRequest): Promise<void> {
const serializableRequest: ISerializableUpdateRequest = { workspace: this.workspace };
if (request.insert) {
serializableRequest.insert = Array.from(request.insert.entries());
}
if (request.delete) {
serializableRequest.delete = Array.from(request.delete.values());
}
return this.channel.call('updateItems', serializableRequest);
}
abstract close(): Promise<void>;
}
class GlobalStorageDatabaseClient extends BaseStorageDatabaseClient implements IStorageDatabase {
private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>());
readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event;
constructor(channel: IChannel) {
super(channel, undefined);
this.registerListeners();
}
private registerListeners(): void {
this._register(this.channel.listen<ISerializableItemsChangeEvent>('onDidChangeGlobalStorage')((e: ISerializableItemsChangeEvent) => this.onDidChangeGlobalStorage(e)));
}
private onDidChangeGlobalStorage(e: ISerializableItemsChangeEvent): void {
if (Array.isArray(e.changed) || Array.isArray(e.deleted)) {
this._onDidChangeItemsExternal.fire({
changed: e.changed ? new Map(e.changed) : undefined,
deleted: e.deleted ? new Set<string>(e.deleted) : undefined
});
}
}
async close(): Promise<void> {
// The global storage database is shared across all instances so
// we do not await it. However we dispose the listener for external
// changes because we no longer interested int 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);
}
async close(): Promise<void> {
const serializableRequest: ISerializableUpdateRequest = { workspace: this.workspace };
return this.channel.call('close', serializableRequest);
}
}
export class StorageDatabaseChannelClient extends Disposable {
private _globalStorage: GlobalStorageDatabaseClient | undefined = undefined;
get globalStorage() {
if (!this._globalStorage) {
this._globalStorage = new GlobalStorageDatabaseClient(this.channel);
}
return this._globalStorage;
}
private _workspaceStorage: WorkspaceStorageDatabaseClient | undefined = undefined;
get workspaceStorage() {
if (!this._workspaceStorage && this.workspace) {
this._workspaceStorage = new WorkspaceStorageDatabaseClient(this.channel, this.workspace);
}
return this._workspaceStorage;
}
constructor(
private channel: IChannel,
private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined
) {
super();
}
}

View File

@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { ILogService } from 'vs/platform/log/common/log';
import { ISerializableItemsChangeEvent, ISerializableUpdateRequest, IBaseSerializableStorageRequest, 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 { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces';
export class StorageDatabaseChannel extends Disposable implements IServerChannel {
private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100;
private readonly _onDidChangeGlobalStorage = this._register(new Emitter<ISerializableItemsChangeEvent>());
private readonly onDidChangeGlobalStorage = this._onDidChangeGlobalStorage.event;
constructor(
private logService: ILogService,
private storageMainService: IStorageMainService
) {
super();
this.registerGlobalStorageListeners();
}
//#region Global Storage Change Events
private registerGlobalStorageListeners(): void {
// Listen for changes in global 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) => {
if (!prev) {
prev = [cur];
} else {
prev.push(cur);
}
return prev;
}, StorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => {
if (events.length) {
this._onDidChangeGlobalStorage.fire(this.serializeGlobalStorageEvents(events));
}
}));
}
private serializeGlobalStorageEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent {
const changed = new Map<Key, Value>();
const deleted = new Set<Key>();
events.forEach(event => {
const existing = this.storageMainService.globalStorage.get(event.key);
if (typeof existing === 'string') {
changed.set(event.key, existing);
} else {
deleted.add(event.key);
}
});
return {
changed: Array.from(changed.entries()),
deleted: Array.from(deleted.values())
};
}
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeGlobalStorage': return this.onDidChangeGlobalStorage;
}
throw new Error(`Event not found: ${event}`);
}
//#endregion
async call(_: unknown, command: string, arg: IBaseSerializableStorageRequest): Promise<any> {
const workspace = reviveIdentifier(arg.workspace);
// Get storage to be ready
const storage = await this.withStorageInitialized(workspace);
// handle call
switch (command) {
case 'getItems': {
return Array.from(storage.items.entries());
}
case 'updateItems': {
const items: ISerializableUpdateRequest = arg;
if (items.insert) {
for (const [key, value] of items.insert) {
storage.set(key, value);
}
}
if (items.delete) {
items.delete.forEach(key => storage.delete(key));
}
break;
}
case 'close': {
// We only allow to close workspace scoped storage because
// global storage is shared across all windows and closes
// only on shutdown.
if (workspace) {
return storage.close();
}
break;
}
default:
throw new Error(`Call not found: ${command}`);
}
}
private async withStorageInitialized(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): Promise<IStorageMain> {
const storage = workspace ? this.storageMainService.workspaceStorage(workspace) : this.storageMainService.globalStorage;
try {
await storage.init();
} catch (error) {
this.logService.error(`StorageIPC#init: Unable to init ${workspace ? 'workspace' : 'global'} storage due to ${error}`);
}
return storage;
}
}

View File

@@ -0,0 +1,307 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises } from 'fs';
import { exists, writeFile } from 'vs/base/node/pfs';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, InMemoryStorageDatabase, StorageHint, IStorage } from 'vs/base/parts/storage/common/storage';
import { join } from 'vs/base/common/path';
import { IS_NEW_KEY } from 'vs/platform/storage/common/storage';
import { currentSessionDateStorageKey, firstSessionDateStorageKey, instanceStorageKey, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry';
import { generateUuid } from 'vs/base/common/uuid';
import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
export interface IStorageMainOptions {
/**
* If enabled, storage will not persist to disk
* but into memory.
*/
useInMemoryStorage?: boolean;
}
/**
* Provides access to global and workspace storage from the
* electron-main side that is the owner of all storage connections.
*/
export interface IStorageMain extends IDisposable {
/**
* Emitted whenever data is updated or deleted.
*/
readonly onDidChangeStorage: Event<IStorageChangeEvent>;
/**
* Emitted when the storage is closed.
*/
readonly onDidCloseStorage: Event<void>;
/**
* Access to all cached items of this storage service.
*/
readonly items: Map<string, string>;
/**
* Required call to ensure the service can be used.
*/
init(): Promise<void>;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined.
*/
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
/**
* Store a string value under the given key to storage. The value will
* be converted to a string.
*/
set(key: string, value: string | boolean | number | undefined | null): void;
/**
* Delete an element stored under the provided key from storage.
*/
delete(key: string): void;
/**
* Close the storage connection.
*/
close(): Promise<void>;
}
export interface IStorageChangeEvent {
key: string;
}
abstract class BaseStorageMain extends Disposable implements IStorageMain {
protected readonly _onDidChangeStorage = this._register(new Emitter<IStorageChangeEvent>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
private readonly _onDidCloseStorage = this._register(new Emitter<void>());
readonly onDidCloseStorage = this._onDidCloseStorage.event;
private storage: IStorage = new Storage(new InMemoryStorageDatabase()); // storage is in-memory until initialized
private initializePromise: Promise<void> | undefined = undefined;
constructor(
protected readonly logService: ILogService
) {
super();
}
init(): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = (async () => {
try {
// Create storage via subclasses
const storage = await this.doCreate();
// Replace our in-memory storage with the real
// once as soon as possible without awaiting
// the init call.
this.storage.dispose();
this.storage = storage;
// Re-emit storage changes via event
this._register(storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key })));
// Await storage init
await this.doInit(storage);
// Ensure we track wether storage is new or not
const isNewStorage = storage.getBoolean(IS_NEW_KEY);
if (isNewStorage === undefined) {
storage.set(IS_NEW_KEY, true);
} else if (isNewStorage) {
storage.set(IS_NEW_KEY, false);
}
} catch (error) {
this.logService.error(`StorageMain#initialize(): Unable to init storage due to ${error}`);
}
})();
}
return this.initializePromise;
}
protected createLogginOptions(): ISQLiteStorageDatabaseLoggingOptions {
return {
logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined,
logError: error => this.logService.error(error)
};
}
protected doInit(storage: IStorage): Promise<void> {
return storage.init();
}
protected abstract doCreate(): Promise<IStorage>;
get items(): Map<string, string> { return this.storage.items; }
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
get(key: string, fallbackValue?: string): string | undefined {
return this.storage.get(key, fallbackValue);
}
set(key: string, value: string | boolean | number | undefined | null): Promise<void> {
return this.storage.set(key, value);
}
delete(key: string): Promise<void> {
return this.storage.delete(key);
}
async close(): Promise<void> {
// Ensure we are not accidentally leaving
// a pending initialized storage behind in
// case close() was called before init()
// finishes
if (this.initializePromise) {
await this.initializePromise;
}
// Propagate to storage lib
await this.storage.close();
// Signal as event
this._onDidCloseStorage.fire();
}
}
export class GlobalStorageMain extends BaseStorageMain implements IStorageMain {
private static readonly STORAGE_NAME = 'state.vscdb';
constructor(
private readonly options: IStorageMainOptions,
logService: ILogService,
private readonly environmentService: IEnvironmentService
) {
super(logService);
}
protected async doCreate(): Promise<IStorage> {
let storagePath: string;
if (this.options.useInMemoryStorage) {
storagePath = SQLiteStorageDatabase.IN_MEMORY_PATH;
} else {
storagePath = join(this.environmentService.globalStorageHome.fsPath, GlobalStorageMain.STORAGE_NAME);
}
return new Storage(new SQLiteStorageDatabase(storagePath, {
logging: this.createLogginOptions()
}));
}
protected async doInit(storage: IStorage): Promise<void> {
await super.doInit(storage);
// Apply global telemetry values as part of the initialization
this.updateTelemetryState(storage);
}
private updateTelemetryState(storage: IStorage): void {
// Instance UUID (once)
const instanceId = storage.get(instanceStorageKey, undefined);
if (instanceId === undefined) {
storage.set(instanceStorageKey, generateUuid());
}
// First session date (once)
const firstSessionDate = storage.get(firstSessionDateStorageKey, undefined);
if (firstSessionDate === undefined) {
storage.set(firstSessionDateStorageKey, new Date().toUTCString());
}
// Last / current session (always)
// previous session date was the "current" one at that time
// current session date is "now"
const lastSessionDate = storage.get(currentSessionDateStorageKey, undefined);
const currentSessionDate = new Date().toUTCString();
storage.set(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate);
storage.set(currentSessionDateStorageKey, currentSessionDate);
}
}
export class WorkspaceStorageMain extends BaseStorageMain implements IStorageMain {
private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb';
private static readonly WORKSPACE_META_NAME = 'workspace.json';
constructor(
private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier,
private readonly options: IStorageMainOptions,
logService: ILogService,
private readonly environmentService: IEnvironmentService
) {
super(logService);
}
protected async doCreate(): Promise<IStorage> {
const { storageFilePath, wasCreated } = await this.prepareWorkspaceStorageFolder();
return new Storage(new SQLiteStorageDatabase(storageFilePath, {
logging: this.createLogginOptions()
}), { hint: wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined });
}
private async prepareWorkspaceStorageFolder(): Promise<{ storageFilePath: string, wasCreated: boolean }> {
// Return early if using inMemory storage
if (this.options.useInMemoryStorage) {
return { storageFilePath: SQLiteStorageDatabase.IN_MEMORY_PATH, wasCreated: true };
}
// Otherwise, ensure the storage folder exists on disk
const workspaceStorageFolderPath = join(this.environmentService.workspaceStorageHome.fsPath, this.workspace.id);
const workspaceStorageDatabasePath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME);
const storageExists = await exists(workspaceStorageFolderPath);
if (storageExists) {
return { storageFilePath: workspaceStorageDatabasePath, wasCreated: false };
}
// Ensure storage folder exists
await promises.mkdir(workspaceStorageFolderPath, { recursive: true });
// Write metadata into folder (but do not await)
this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath);
return { storageFilePath: workspaceStorageDatabasePath, wasCreated: true };
}
private async ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath: string): Promise<void> {
let meta: object | undefined = undefined;
if (isSingleFolderWorkspaceIdentifier(this.workspace)) {
meta = { folder: this.workspace.uri.toString() };
} else if (isWorkspaceIdentifier(this.workspace)) {
meta = { workspace: this.workspace.configPath.toString() };
}
if (meta) {
try {
const workspaceStorageMetaPath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_META_NAME);
const storageExists = await exists(workspaceStorageMetaPath);
if (!storageExists) {
await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2));
}
} catch (error) {
this.logService.error(`StorageMain#ensureWorkspaceStorageFolderMeta(): Unable to create workspace storage metadata due to ${error}`);
}
}
}
}

View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { once } from 'vs/base/common/functional';
import { Disposable } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
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 { GlobalStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain } from 'vs/platform/storage/electron-main/storageMain';
import { IWindowSettings } from 'vs/platform/windows/common/windows';
import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
export const IStorageMainService = createDecorator<IStorageMainService>('storageMainService');
export interface IStorageMainService {
readonly _serviceBrand: undefined;
/**
* Provides access to the global storage shared across all windows.
*/
readonly globalStorage: IStorageMain;
/**
* Provides access to the workspace storage specific to a single window.
*/
workspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain;
}
export class StorageMainService extends Disposable implements IStorageMainService {
declare readonly _serviceBrand: undefined;
constructor(
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super();
this.registerListeners();
}
protected getStorageOptions(): IStorageMainOptions {
return {
useInMemoryStorage: !!this.environmentService.extensionTestsLocationURI // no storage during extension tests!
};
}
protected enableMainWorkspaceStorage(): boolean {
return !!(this.configurationService.getValue<IWindowSettings | undefined>('window')?.enableExperimentalMainProcessWorkspaceStorage);
}
private registerListeners(): void {
// Global Storage: Warmup when any window opens
(async () => {
await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen);
this.globalStorage.init();
})();
// Workspace Storage: Warmup when related window with workspace loads
if (this.enableMainWorkspaceStorage()) {
this._register(this.lifecycleMainService.onWillLoadWindow(async e => {
if (e.workspace) {
this.workspaceStorage(e.workspace).init();
}
}));
}
// All Storage: Close when shutting down
this._register(this.lifecycleMainService.onWillShutdown(e => {
// Global Storage
e.join(this.globalStorage.close());
// Workspace Storage(s)
for (const [, storage] of this.mapWorkspaceToStorage) {
e.join(storage.close());
}
}));
}
//#region Global Storage
readonly globalStorage = this.createGlobalStorage();
private createGlobalStorage(): IStorageMain {
if (this.globalStorage) {
return this.globalStorage; // only once
}
this.logService.trace(`StorageMainService: creating global storage`);
const globalStorage = new GlobalStorageMain(this.getStorageOptions(), this.logService, this.environmentService);
once(globalStorage.onDidCloseStorage)(() => {
this.logService.trace(`StorageMainService: closed global storage`);
});
return globalStorage;
}
//#endregion
//#region Workspace Storage
private readonly mapWorkspaceToStorage = new Map<string, IStorageMain>();
private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain {
const workspaceStorage = new WorkspaceStorageMain(workspace, this.getStorageOptions(), this.logService, this.environmentService);
return workspaceStorage;
}
workspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain {
let workspaceStorage = this.mapWorkspaceToStorage.get(workspace.id);
if (!workspaceStorage) {
this.logService.trace(`StorageMainService: creating workspace storage (${workspace.id})`);
workspaceStorage = this.createWorkspaceStorage(workspace);
this.mapWorkspaceToStorage.set(workspace.id, workspaceStorage);
once(workspaceStorage.onDidCloseStorage)(() => {
this.logService.trace(`StorageMainService: closed workspace storage (${workspace.id})`);
this.mapWorkspaceToStorage.delete(workspace.id);
});
}
return workspaceStorage;
}
//#endregion
}

View File

@@ -0,0 +1,127 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MutableDisposable } from 'vs/base/common/lifecycle';
import { StorageScope, WillSaveStateReason, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { Storage, IStorage } from 'vs/base/parts/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { Promises } from 'vs/base/common/async';
import { mark } from 'vs/base/common/performance';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { StorageDatabaseChannelClient } from 'vs/platform/storage/common/storageIpc';
import { joinPath } from 'vs/base/common/resources';
export class NativeStorageService2 extends AbstractStorageService {
// Global Storage is readonly and shared across windows
private readonly globalStorage: IStorage;
// Workspace Storage is scoped to a window but can change
// in the current window, when entering a workspace!
private workspaceStorage: IStorage | undefined = undefined;
private workspaceStorageId: string | undefined = undefined;
private workspaceStorageDisposable = this._register(new MutableDisposable());
constructor(
workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined,
private readonly mainProcessService: IMainProcessService,
private readonly environmentService: IEnvironmentService
) {
super();
this.globalStorage = this.createGlobalStorage();
this.workspaceStorage = this.createWorkspaceStorage(workspace);
}
private createGlobalStorage(): IStorage {
const storageDataBaseClient = new StorageDatabaseChannelClient(this.mainProcessService.getChannel('storage'), undefined);
const globalStorage = new Storage(storageDataBaseClient.globalStorage);
this._register(globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
return globalStorage;
}
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);
if (storageDataBaseClient.workspaceStorage) {
const workspaceStorage = new Storage(storageDataBaseClient.workspaceStorage);
this.workspaceStorageDisposable.value = workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key));
this.workspaceStorageId = workspace?.id;
return workspaceStorage;
} else {
this.workspaceStorageDisposable.clear();
this.workspaceStorageId = undefined;
return undefined;
}
}
protected async doInitialize(): Promise<void> {
// Init all storage locations
mark('code/willInitStorage');
try {
await Promises.settled([
this.globalStorage.init(),
this.workspaceStorage?.init() ?? Promise.resolve()
]);
} finally {
mark('code/didInitStorage');
}
}
protected getStorage(scope: StorageScope): IStorage | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
}
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? this.environmentService.globalStorageHome.fsPath : this.workspaceStorageId ? `${joinPath(this.environmentService.workspaceStorageHome, this.workspaceStorageId, 'state.vscdb').fsPath} [!!! Experimental Main Storage !!!]` : undefined;
}
async close(): Promise<void> {
// Stop periodic scheduler and idle runner as we now collect state normally
this.stopFlushWhenIdle();
// Signal as event so that clients can still store data
this.emitWillSaveState(WillSaveStateReason.SHUTDOWN);
// Do it
await Promises.settled([
this.globalStorage.close(),
this.workspaceStorage?.close() ?? Promise.resolve()
]);
}
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
// Keep current workspace storage items around to restore
const oldWorkspaceStorage = this.workspaceStorage;
const oldItems = oldWorkspaceStorage?.items ?? new Map();
// Close current which will change to new workspace storage
if (oldWorkspaceStorage) {
await oldWorkspaceStorage.close();
oldWorkspaceStorage.dispose();
}
// Create new workspace storage & init
this.workspaceStorage = this.createWorkspaceStorage(toWorkspace);
await this.workspaceStorage.init();
// Copy over previous keys
for (const [key, value] of oldItems) {
this.workspaceStorage.set(key, value);
}
}
}

View File

@@ -1,214 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event, Emitter } from 'vs/base/common/event';
import { IStorageChangeEvent, IStorageMainService } from 'vs/platform/storage/node/storageMainService';
import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { generateUuid } from 'vs/base/common/uuid';
import { instanceStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey, currentSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry';
type Key = string;
type Value = string;
type Item = [Key, Value];
interface ISerializableUpdateRequest {
insert?: Item[];
delete?: Key[];
}
interface ISerializableItemsChangeEvent {
readonly changed?: Item[];
readonly deleted?: Key[];
}
export class GlobalStorageDatabaseChannel extends Disposable implements IServerChannel {
private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100;
private readonly _onDidChangeItems = this._register(new Emitter<ISerializableItemsChangeEvent>());
readonly onDidChangeItems = this._onDidChangeItems.event;
private readonly whenReady = this.init();
constructor(
private logService: ILogService,
private storageMainService: IStorageMainService
) {
super();
}
private async init(): Promise<void> {
try {
await this.storageMainService.initialize();
} catch (error) {
this.logService.error(`[storage] init(): Unable to init global storage due to ${error}`);
}
// Apply global telemetry values as part of the initialization
// These are global across all windows and thereby should be
// written from the main process once.
this.initTelemetry();
// Setup storage change listeners
this.registerListeners();
}
private initTelemetry(): void {
const instanceId = this.storageMainService.get(instanceStorageKey, undefined);
if (instanceId === undefined) {
this.storageMainService.store(instanceStorageKey, generateUuid());
}
const firstSessionDate = this.storageMainService.get(firstSessionDateStorageKey, undefined);
if (firstSessionDate === undefined) {
this.storageMainService.store(firstSessionDateStorageKey, new Date().toUTCString());
}
const lastSessionDate = this.storageMainService.get(currentSessionDateStorageKey, undefined); // previous session date was the "current" one at that time
const currentSessionDate = new Date().toUTCString(); // current session date is "now"
this.storageMainService.store(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate);
this.storageMainService.store(currentSessionDateStorageKey, currentSessionDate);
}
private registerListeners(): void {
// Listen for changes in global storage to send to listeners
// that are listening. Use a debouncer to reduce IPC traffic.
this._register(Event.debounce(this.storageMainService.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => {
if (!prev) {
prev = [cur];
} else {
prev.push(cur);
}
return prev;
}, GlobalStorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => {
if (events.length) {
this._onDidChangeItems.fire(this.serializeEvents(events));
}
}));
}
private serializeEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent {
const changed = new Map<Key, Value>();
const deleted = new Set<Key>();
events.forEach(event => {
const existing = this.storageMainService.get(event.key);
if (typeof existing === 'string') {
changed.set(event.key, existing);
} else {
deleted.add(event.key);
}
});
return {
changed: Array.from(changed.entries()),
deleted: Array.from(deleted.values())
};
}
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeItems': return this.onDidChangeItems;
}
throw new Error(`Event not found: ${event}`);
}
async call(_: unknown, command: string, arg?: any): Promise<any> {
// ensure to always wait for ready
await this.whenReady;
// handle call
switch (command) {
case 'getItems': {
return Array.from(this.storageMainService.items.entries());
}
case 'updateItems': {
const items: ISerializableUpdateRequest = arg;
if (items.insert) {
for (const [key, value] of items.insert) {
this.storageMainService.store(key, value);
}
}
if (items.delete) {
items.delete.forEach(key => this.storageMainService.remove(key));
}
break;
}
default:
throw new Error(`Call not found: ${command}`);
}
}
}
export class GlobalStorageDatabaseChannelClient extends Disposable implements IStorageDatabase {
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>());
readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event;
private onDidChangeItemsOnMainListener: IDisposable | undefined;
constructor(private channel: IChannel) {
super();
this.registerListeners();
}
private registerListeners(): void {
this.onDidChangeItemsOnMainListener = this.channel.listen<ISerializableItemsChangeEvent>('onDidChangeItems')((e: ISerializableItemsChangeEvent) => this.onDidChangeItemsOnMain(e));
}
private onDidChangeItemsOnMain(e: ISerializableItemsChangeEvent): void {
if (Array.isArray(e.changed) || Array.isArray(e.deleted)) {
this._onDidChangeItemsExternal.fire({
changed: e.changed ? new Map(e.changed) : undefined,
deleted: e.deleted ? new Set<string>(e.deleted) : undefined
});
}
}
async getItems(): Promise<Map<string, string>> {
const items: Item[] = await this.channel.call('getItems');
return new Map(items);
}
updateItems(request: IUpdateRequest): Promise<void> {
const serializableRequest: ISerializableUpdateRequest = Object.create(null);
if (request.insert) {
serializableRequest.insert = Array.from(request.insert.entries());
}
if (request.delete) {
serializableRequest.delete = Array.from(request.delete.values());
}
return this.channel.call('updateItems', serializableRequest);
}
async close(): Promise<void> {
// when we are about to close, we start to ignore main-side changes since we close anyway
dispose(this.onDidChangeItemsOnMainListener);
}
dispose(): void {
super.dispose();
dispose(this.onDidChangeItemsOnMainListener);
}
}

View File

@@ -1,191 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, IStorage, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
import { join } from 'vs/base/common/path';
import { IS_NEW_KEY } from 'vs/platform/storage/common/storage';
export const IStorageMainService = createDecorator<IStorageMainService>('storageMainService');
export interface IStorageMainService {
readonly _serviceBrand: undefined;
/**
* Emitted whenever data is updated or deleted.
*/
readonly onDidChangeStorage: Event<IStorageChangeEvent>;
/**
* Emitted when the storage is about to persist. This is the right time
* to persist data to ensure it is stored before the application shuts
* down.
*
* Note: this event may be fired many times, not only on shutdown to prevent
* loss of state in situations where the shutdown is not sufficient to
* persist the data properly.
*/
readonly onWillSaveState: Event<void>;
/**
* Access to all cached items of this storage service.
*/
readonly items: Map<string, string>;
/**
* Required call to ensure the service can be used.
*/
initialize(): Promise<void>;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined.
*/
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a boolean.
*/
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a number using parseInt with a base of 10.
*/
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
/**
* Store a string value under the given key to storage. The value will
* be converted to a string.
*/
store(key: string, value: string | boolean | number | undefined | null): void;
/**
* Delete an element stored under the provided key from storage.
*/
remove(key: string): void;
}
export interface IStorageChangeEvent {
key: string;
}
export class StorageMainService extends Disposable implements IStorageMainService {
declare readonly _serviceBrand: undefined;
private static readonly STORAGE_NAME = 'state.vscdb';
private readonly _onDidChangeStorage = this._register(new Emitter<IStorageChangeEvent>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
private readonly _onWillSaveState = this._register(new Emitter<void>());
readonly onWillSaveState = this._onWillSaveState.event;
get items(): Map<string, string> { return this.storage.items; }
private storage: IStorage;
private initializePromise: Promise<void> | undefined;
constructor(
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) {
super();
// Until the storage has been initialized, it can only be in memory
this.storage = new Storage(new InMemoryStorageDatabase());
}
private get storagePath(): string {
if (!!this.environmentService.extensionTestsLocationURI) {
return SQLiteStorageDatabase.IN_MEMORY_PATH; // no storage during extension tests!
}
return join(this.environmentService.globalStorageHome.fsPath, StorageMainService.STORAGE_NAME);
}
private createLogginOptions(): ISQLiteStorageDatabaseLoggingOptions {
return {
logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined,
logError: error => this.logService.error(error)
};
}
initialize(): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = this.doInitialize();
}
return this.initializePromise;
}
private async doInitialize(): Promise<void> {
this.storage.dispose();
this.storage = new Storage(new SQLiteStorageDatabase(this.storagePath, {
logging: this.createLogginOptions()
}));
this._register(this.storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key })));
await this.storage.init();
// Check to see if this is the first time we are "opening" the application
const firstOpen = this.storage.getBoolean(IS_NEW_KEY);
if (firstOpen === undefined) {
this.storage.set(IS_NEW_KEY, true);
} else if (firstOpen) {
this.storage.set(IS_NEW_KEY, false);
}
}
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
get(key: string, fallbackValue?: string): string | undefined {
return this.storage.get(key, fallbackValue);
}
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
return this.storage.getBoolean(key, fallbackValue);
}
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
getNumber(key: string, fallbackValue?: number): number | undefined {
return this.storage.getNumber(key, fallbackValue);
}
store(key: string, value: string | boolean | number | undefined | null): Promise<void> {
return this.storage.set(key, value);
}
remove(key: string): Promise<void> {
return this.storage.delete(key);
}
close(): Promise<void> {
// Signal as event so that clients can still store data
this._onWillSaveState.fire();
// Do it
return this.storage.close();
}
}

View File

@@ -3,18 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises } from 'fs';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { StorageScope, WillSaveStateReason, logStorage, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { StorageScope, WillSaveStateReason, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage';
import { mark } from 'vs/base/common/performance';
import { join } from 'vs/base/common/path';
import { copy, exists, mkdirp, writeFile } from 'vs/base/node/pfs';
import { copy, exists, writeFile } from 'vs/base/node/pfs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { assertIsDefined } from 'vs/base/common/types';
import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
import { Promises } from 'vs/base/common/async';
export class NativeStorageService extends AbstractStorageService {
@@ -27,13 +28,9 @@ export class NativeStorageService extends AbstractStorageService {
private workspaceStorage: IStorage | undefined;
private workspaceStorageListener: IDisposable | undefined;
private initializePromise: Promise<void> | undefined;
private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 60000 /* every minute */));
private runWhenIdleDisposable: IDisposable | undefined = undefined;
constructor(
private globalStorageDatabase: IStorageDatabase,
private payload: IWorkspaceInitializationPayload | undefined,
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) {
@@ -48,26 +45,13 @@ export class NativeStorageService extends AbstractStorageService {
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
}
initialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = this.doInitialize(payload);
}
return this.initializePromise;
}
private async doInitialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
protected async doInitialize(): Promise<void> {
// Init all storage locations
await Promise.all([
await Promises.settled([
this.initializeGlobalStorage(),
payload ? this.initializeWorkspaceStorage(payload) : Promise.resolve()
this.payload ? this.initializeWorkspaceStorage(this.payload) : Promise.resolve()
]);
// On some OS we do not get enough time to persist state on shutdown (e.g. when
// Windows restarts after applying updates). In other cases, VSCode might crash,
// so we periodically save state to reduce the chance of loosing any state.
this.periodicFlushScheduler.schedule();
}
private initializeGlobalStorage(): Promise<void> {
@@ -138,7 +122,7 @@ export class NativeStorageService extends AbstractStorageService {
return { path: workspaceStorageFolderPath, wasCreated: false };
}
await mkdirp(workspaceStorageFolderPath);
await promises.mkdir(workspaceStorageFolderPath, { recursive: true });
// Write metadata into folder
this.ensureWorkspaceStorageFolderMeta(payload);
@@ -169,6 +153,7 @@ export class NativeStorageService extends AbstractStorageService {
}
}
<<<<<<< HEAD
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope): string | undefined;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
@@ -193,73 +178,38 @@ export class NativeStorageService extends AbstractStorageService {
protected doRemove(key: string, scope: StorageScope): void {
this.getStorage(scope).delete(key);
=======
protected getStorage(scope: StorageScope): IStorage | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
>>>>>>> e8cd17a97d8c58fffcbac05394b3ee2b3c72d384
}
private getStorage(scope: StorageScope): IStorage {
return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage);
}
protected async doFlush(): Promise<void> {
const promises: Promise<unknown>[] = [];
if (this.globalStorage) {
promises.push(this.globalStorage.whenFlushed());
}
if (this.workspaceStorage) {
promises.push(this.workspaceStorage.whenFlushed());
}
await Promise.all(promises);
}
private doFlushWhenIdle(): void {
// Dispose any previous idle runner
dispose(this.runWhenIdleDisposable);
// Run when idle
this.runWhenIdleDisposable = runWhenIdle(() => {
// send event to collect state
this.flush();
// repeat
this.periodicFlushScheduler.schedule();
});
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? this.environmentService.globalStorageHome.fsPath : this.workspaceStoragePath;
}
async close(): Promise<void> {
// Stop periodic scheduler and idle runner as we now collect state normally
this.periodicFlushScheduler.dispose();
dispose(this.runWhenIdleDisposable);
this.runWhenIdleDisposable = undefined;
this.stopFlushWhenIdle();
// Signal as event so that clients can still store data
this.emitWillSaveState(WillSaveStateReason.SHUTDOWN);
// Do it
await Promise.all([
await Promises.settled([
this.globalStorage.close(),
this.workspaceStorage ? this.workspaceStorage.close() : Promise.resolve()
]);
}
async logStorage(): Promise<void> {
return logStorage(
this.globalStorage.items,
this.workspaceStorage ? this.workspaceStorage.items : new Map<string, string>(), // Shared process storage does not has workspace storage
this.environmentService.globalStorageHome.fsPath,
this.workspaceStoragePath || '');
}
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
if (this.workspaceStoragePath === SQLiteStorageDatabase.IN_MEMORY_PATH) {
return; // no migration needed if running in memory
}
// Close workspace DB to be able to copy
await this.getStorage(StorageScope.WORKSPACE).close();
await this.workspaceStorage?.close();
// Prepare new workspace storage folder
const result = await this.prepareWorkspaceStorageFolder(toWorkspace);
@@ -267,7 +217,7 @@ export class NativeStorageService extends AbstractStorageService {
const newWorkspaceStoragePath = join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME);
// Copy current storage over to new workspace storage
await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath);
await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath, { preserveSymlinks: false });
// Recreate and init workspace storage
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();

View File

@@ -4,49 +4,67 @@
*--------------------------------------------------------------------------------------------*/
import { strictEqual } from 'assert';
import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService';
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';
import { rimraf } from 'vs/base/node/pfs';
import { BrowserStorageService, FileStorageDatabase } from 'vs/platform/storage/browser/storageService';
import { NullLogService } from 'vs/platform/log/common/log';
import { Storage } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { FileService } from 'vs/platform/files/common/fileService';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { createSuite } from 'vs/platform/storage/test/common/storageService.test';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
suite('Storage', () => {
suite('StorageService (browser)', function () {
let testDir: string;
const disposables = new DisposableStore();
let storageService: BrowserStorageService;
createSuite<BrowserStorageService>({
setup: async () => {
const logService = new NullLogService();
const fileService = disposables.add(new FileService(logService));
const userDataProvider = disposables.add(new InMemoryFileSystemProvider());
disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider));
storageService = disposables.add(new BrowserStorageService({ id: String(Date.now()) }, { userRoamingDataHome: URI.file('/User').with({ scheme: Schemas.userData }) } as unknown as IEnvironmentService, fileService));
await storageService.initialize();
return storageService;
},
teardown: async storage => {
await storageService.flush();
disposables.clear();
}
});
});
suite('FileStorageDatabase (browser)', () => {
let fileService: FileService;
let fileProvider: DiskFileSystemProvider;
const disposables = new DisposableStore();
setup(async () => {
const logService = new NullLogService();
fileService = new FileService(logService);
disposables.add(fileService);
fileService = disposables.add(new FileService(logService));
fileProvider = new DiskFileSystemProvider(logService);
disposables.add(fileService.registerProvider(Schemas.file, fileProvider));
disposables.add(fileProvider);
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
const userDataProvider = disposables.add(new InMemoryFileSystemProvider());
disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider));
});
teardown(() => {
disposables.clear();
return rimraf(testDir);
});
test('File Based Storage', async () => {
let storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
test('Basics', async () => {
const testDir = URI.file('/User/storage.json').with({ scheme: Schemas.userData });
let storage = new Storage(new FileStorageDatabase(testDir, false, fileService));
await storage.init();
@@ -60,7 +78,7 @@ suite('Storage', () => {
await storage.close();
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
storage = new Storage(new FileStorageDatabase(testDir, false, fileService));
await storage.init();
@@ -78,7 +96,7 @@ suite('Storage', () => {
await storage.close();
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
storage = new Storage(new FileStorageDatabase(testDir, false, fileService));
await storage.init();

View File

@@ -4,98 +4,102 @@
*--------------------------------------------------------------------------------------------*/
import { strictEqual, ok } from 'assert';
import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent } from 'vs/platform/storage/common/storage';
import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent, IStorageService } from 'vs/platform/storage/common/storage';
suite('StorageService', function () {
export function createSuite<T extends IStorageService>(params: { setup: () => Promise<T>, teardown: (service: T) => Promise<void> }): void {
test('Get Data, Integer, Boolean (global, in-memory)', () => {
let storageService: T;
setup(async () => {
storageService = await params.setup();
});
teardown(() => {
return params.teardown(storageService);
});
test('Get Data, Integer, Boolean (global)', () => {
storeData(StorageScope.GLOBAL);
});
test('Get Data, Integer, Boolean (workspace, in-memory)', () => {
test('Get Data, Integer, Boolean (workspace)', () => {
storeData(StorageScope.WORKSPACE);
});
function storeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
let storageValueChangeEvents: IStorageValueChangeEvent[] = [];
storage.onDidChangeValue(e => storageValueChangeEvents.push(e));
storageService.onDidChangeValue(e => storageValueChangeEvents.push(e));
strictEqual(storage.get('test.get', scope, 'foobar'), 'foobar');
strictEqual(storage.get('test.get', scope, ''), '');
strictEqual(storage.getNumber('test.getNumber', scope, 5), 5);
strictEqual(storage.getNumber('test.getNumber', scope, 0), 0);
strictEqual(storage.getBoolean('test.getBoolean', scope, true), true);
strictEqual(storage.getBoolean('test.getBoolean', scope, false), false);
strictEqual(storageService.get('test.get', scope, 'foobar'), 'foobar');
strictEqual(storageService.get('test.get', scope, ''), '');
strictEqual(storageService.getNumber('test.getNumber', scope, 5), 5);
strictEqual(storageService.getNumber('test.getNumber', scope, 0), 0);
strictEqual(storageService.getBoolean('test.getBoolean', scope, true), true);
strictEqual(storageService.getBoolean('test.getBoolean', scope, false), false);
storage.store('test.get', 'foobar', scope, StorageTarget.MACHINE);
strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar');
storageService.store('test.get', 'foobar', scope, StorageTarget.MACHINE);
strictEqual(storageService.get('test.get', scope, (undefined)!), 'foobar');
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.get');
storageValueChangeEvents = [];
storage.store('test.get', '', scope, StorageTarget.MACHINE);
strictEqual(storage.get('test.get', scope, (undefined)!), '');
storageService.store('test.get', '', scope, StorageTarget.MACHINE);
strictEqual(storageService.get('test.get', scope, (undefined)!), '');
storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
strictEqual(storageValueChangeEvent!.scope, scope);
strictEqual(storageValueChangeEvent!.key, 'test.get');
storage.store('test.getNumber', 5, scope, StorageTarget.MACHINE);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5);
storageService.store('test.getNumber', 5, scope, StorageTarget.MACHINE);
strictEqual(storageService.getNumber('test.getNumber', scope, (undefined)!), 5);
storage.store('test.getNumber', 0, scope, StorageTarget.MACHINE);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0);
storageService.store('test.getNumber', 0, scope, StorageTarget.MACHINE);
strictEqual(storageService.getNumber('test.getNumber', scope, (undefined)!), 0);
storage.store('test.getBoolean', true, scope, StorageTarget.MACHINE);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true);
storageService.store('test.getBoolean', true, scope, StorageTarget.MACHINE);
strictEqual(storageService.getBoolean('test.getBoolean', scope, (undefined)!), true);
storage.store('test.getBoolean', false, scope, StorageTarget.MACHINE);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), false);
storageService.store('test.getBoolean', false, scope, StorageTarget.MACHINE);
strictEqual(storageService.getBoolean('test.getBoolean', scope, (undefined)!), false);
strictEqual(storage.get('test.getDefault', scope, 'getDefault'), 'getDefault');
strictEqual(storage.getNumber('test.getNumberDefault', scope, 5), 5);
strictEqual(storage.getBoolean('test.getBooleanDefault', scope, true), true);
strictEqual(storageService.get('test.getDefault', scope, 'getDefault'), 'getDefault');
strictEqual(storageService.getNumber('test.getNumberDefault', scope, 5), 5);
strictEqual(storageService.getBoolean('test.getBooleanDefault', scope, true), true);
}
test('Remove Data (global, in-memory)', () => {
test('Remove Data (global)', () => {
removeData(StorageScope.GLOBAL);
});
test('Remove Data (workspace, in-memory)', () => {
test('Remove Data (workspace)', () => {
removeData(StorageScope.WORKSPACE);
});
function removeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
let storageValueChangeEvents: IStorageValueChangeEvent[] = [];
storage.onDidChangeValue(e => storageValueChangeEvents.push(e));
storageService.onDidChangeValue(e => storageValueChangeEvents.push(e));
storage.store('test.remove', 'foobar', scope, StorageTarget.MACHINE);
strictEqual('foobar', storage.get('test.remove', scope, (undefined)!));
storageService.store('test.remove', 'foobar', scope, StorageTarget.MACHINE);
strictEqual('foobar', storageService.get('test.remove', scope, (undefined)!));
storage.remove('test.remove', scope);
ok(!storage.get('test.remove', scope, (undefined)!));
storageService.remove('test.remove', scope);
ok(!storageService.get('test.remove', scope, (undefined)!));
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.remove');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.remove');
}
test('Keys (in-memory)', () => {
const storage = new InMemoryStorageService();
let storageTargetEvent: IStorageTargetChangeEvent | undefined = undefined;
storage.onDidChangeTarget(e => storageTargetEvent = e);
storageService.onDidChangeTarget(e => storageTargetEvent = e);
let storageValueChangeEvent: IStorageValueChangeEvent | undefined = undefined;
storage.onDidChangeValue(e => storageValueChangeEvent = e);
storageService.onDidChangeValue(e => storageValueChangeEvent = e);
// Empty
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
strictEqual(storage.keys(scope, target).length, 0);
strictEqual(storageService.keys(scope, target).length, 0);
}
}
@@ -105,8 +109,8 @@ suite('StorageService', function () {
storageTargetEvent = Object.create(null);
storageValueChangeEvent = Object.create(null);
storage.store('test.target1', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storageService.store('test.target1', 'value1', scope, target);
strictEqual(storageService.keys(scope, target).length, 1);
strictEqual(storageTargetEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.target1');
strictEqual(storageValueChangeEvent?.scope, scope);
@@ -115,33 +119,33 @@ suite('StorageService', function () {
storageTargetEvent = undefined;
storageValueChangeEvent = Object.create(null);
storage.store('test.target1', 'otherValue1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storageService.store('test.target1', 'otherValue1', scope, target);
strictEqual(storageService.keys(scope, target).length, 1);
strictEqual(storageTargetEvent, undefined);
strictEqual(storageValueChangeEvent?.key, 'test.target1');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.target, target);
storage.store('test.target2', 'value2', scope, target);
storage.store('test.target3', 'value3', scope, target);
storageService.store('test.target2', 'value2', scope, target);
storageService.store('test.target3', 'value3', scope, target);
strictEqual(storage.keys(scope, target).length, 3);
strictEqual(storageService.keys(scope, target).length, 3);
}
}
// Remove values
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
const keysLength = storage.keys(scope, target).length;
const keysLength = storageService.keys(scope, target).length;
storage.store('test.target4', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, keysLength + 1);
storageService.store('test.target4', 'value1', scope, target);
strictEqual(storageService.keys(scope, target).length, keysLength + 1);
storageTargetEvent = Object.create(null);
storageValueChangeEvent = Object.create(null);
storage.remove('test.target4', scope);
strictEqual(storage.keys(scope, target).length, keysLength);
storageService.remove('test.target4', scope);
strictEqual(storageService.keys(scope, target).length, keysLength);
strictEqual(storageTargetEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.target4');
strictEqual(storageValueChangeEvent?.scope, scope);
@@ -151,48 +155,55 @@ suite('StorageService', function () {
// Remove all
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
const keys = storage.keys(scope, target);
const keys = storageService.keys(scope, target);
for (const key of keys) {
storage.remove(key, scope);
storageService.remove(key, scope);
}
strictEqual(storage.keys(scope, target).length, 0);
strictEqual(storageService.keys(scope, target).length, 0);
}
}
// Adding undefined or null removes value
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
storage.store('test.target1', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storageService.store('test.target1', 'value1', scope, target);
strictEqual(storageService.keys(scope, target).length, 1);
storageTargetEvent = Object.create(null);
storage.store('test.target1', undefined, scope, target);
strictEqual(storage.keys(scope, target).length, 0);
storageService.store('test.target1', undefined, scope, target);
strictEqual(storageService.keys(scope, target).length, 0);
strictEqual(storageTargetEvent?.scope, scope);
storage.store('test.target1', '', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storageService.store('test.target1', '', scope, target);
strictEqual(storageService.keys(scope, target).length, 1);
storage.store('test.target1', null, scope, target);
strictEqual(storage.keys(scope, target).length, 0);
storageService.store('test.target1', null, scope, target);
strictEqual(storageService.keys(scope, target).length, 0);
}
}
// Target change
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
ok(storageTargetEvent);
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.USER);
storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.USER);
ok(storageTargetEvent);
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
ok(storageTargetEvent);
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
ok(!storageTargetEvent); // no change in target
});
}
suite('StorageService (in-memory)', function () {
createSuite<InMemoryStorageService>({
setup: async () => new InMemoryStorageService(),
teardown: async () => { }
});
});

View File

@@ -0,0 +1,225 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { notStrictEqual, strictEqual } from 'assert';
import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { NullLogService } from 'vs/platform/log/common/log';
import { StorageMainService } from 'vs/platform/storage/electron-main/storageMainService';
import { currentSessionDateStorageKey, firstSessionDateStorageKey, instanceStorageKey } from 'vs/platform/telemetry/common/telemetry';
import { IStorageChangeEvent, IStorageMain, IStorageMainOptions } from 'vs/platform/storage/electron-main/storageMain';
import { generateUuid } from 'vs/base/common/uuid';
import { IS_NEW_KEY } from 'vs/platform/storage/common/storage';
import { ILifecycleMainService, LifecycleMainPhase, ShutdownEvent, UnloadReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { Emitter, Event } from 'vs/base/common/event';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { Promises } from 'vs/base/common/async';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
suite('StorageMainService', function () {
class TestStorageMainService extends StorageMainService {
protected getStorageOptions(): IStorageMainOptions {
return {
useInMemoryStorage: true
};
}
protected enableMainWorkspaceStorage(): boolean {
return true;
}
}
class StorageTestLifecycleMainService implements ILifecycleMainService {
_serviceBrand: undefined;
onBeforeShutdown = Event.None;
private readonly _onWillShutdown = new Emitter<ShutdownEvent>();
readonly onWillShutdown = this._onWillShutdown.event;
async fireOnWillShutdown(): Promise<void> {
const joiners: Promise<void>[] = [];
this._onWillShutdown.fire({
join(promise) {
if (promise) {
joiners.push(promise);
}
}
});
await Promises.settled(joiners);
}
onWillLoadWindow = Event.None;
onBeforeCloseWindow = Event.None;
onBeforeUnloadWindow = Event.None;
wasRestarted = false;
quitRequested = false;
phase = LifecycleMainPhase.Ready;
registerWindow(window: ICodeWindow): void { }
async reload(window: ICodeWindow, cli?: NativeParsedArgs): Promise<void> { }
async unload(window: ICodeWindow, reason: UnloadReason): Promise<boolean> { return true; }
relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined; }): void { }
async quit(fromUpdate?: boolean): Promise<boolean> { return true; }
async kill(code?: number): Promise<void> { }
async when(phase: LifecycleMainPhase): Promise<void> { }
}
async function testStorage(storage: IStorageMain, isGlobal: boolean): Promise<void> {
// Telemetry: added after init
if (isGlobal) {
strictEqual(storage.items.size, 0);
strictEqual(storage.get(instanceStorageKey), undefined);
await storage.init();
strictEqual(typeof storage.get(instanceStorageKey), 'string');
strictEqual(typeof storage.get(firstSessionDateStorageKey), 'string');
strictEqual(typeof storage.get(currentSessionDateStorageKey), 'string');
} else {
await storage.init();
}
let storageChangeEvent: IStorageChangeEvent | undefined = undefined;
const storageChangeListener = storage.onDidChangeStorage(e => {
storageChangeEvent = e;
});
let storageDidClose = false;
const storageCloseListener = storage.onDidCloseStorage(() => storageDidClose = true);
// Basic store/get/remove
const size = storage.items.size;
storage.set('bar', 'foo');
strictEqual(storageChangeEvent!.key, 'bar');
storage.set('barNumber', 55);
storage.set('barBoolean', true);
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.get('barNumber'), '55');
strictEqual(storage.get('barBoolean'), 'true');
strictEqual(storage.items.size, size + 3);
storage.delete('bar');
strictEqual(storage.get('bar'), undefined);
strictEqual(storage.items.size, size + 2);
// IS_NEW
strictEqual(storage.get(IS_NEW_KEY), 'true');
// Close
await storage.close();
strictEqual(storageDidClose, true);
storageChangeListener.dispose();
storageCloseListener.dispose();
}
test('basics (global)', function () {
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService());
return testStorage(storageMainService.globalStorage, true);
});
test('basics (workspace)', function () {
const workspace = { id: generateUuid() };
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService());
return testStorage(storageMainService.workspaceStorage(workspace), false);
});
test('storage closed onWillShutdown', async function () {
const lifecycleMainService = new StorageTestLifecycleMainService();
const workspace = { id: generateUuid() };
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), lifecycleMainService, new TestConfigurationService());
let workspaceStorage = storageMainService.workspaceStorage(workspace);
let didCloseWorkspaceStorage = false;
workspaceStorage.onDidCloseStorage(() => {
didCloseWorkspaceStorage = true;
});
let globalStorage = storageMainService.globalStorage;
let didCloseGlobalStorage = false;
globalStorage.onDidCloseStorage(() => {
didCloseGlobalStorage = true;
});
strictEqual(workspaceStorage, storageMainService.workspaceStorage(workspace)); // same instance as long as not closed
await globalStorage.init();
await workspaceStorage.init();
await lifecycleMainService.fireOnWillShutdown();
strictEqual(didCloseGlobalStorage, true);
strictEqual(didCloseWorkspaceStorage, true);
let storage2 = storageMainService.workspaceStorage(workspace);
notStrictEqual(workspaceStorage, storage2);
return storage2.close();
});
test('storage closed before init works', async function () {
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService());
const workspace = { id: generateUuid() };
let workspaceStorage = storageMainService.workspaceStorage(workspace);
let didCloseWorkspaceStorage = false;
workspaceStorage.onDidCloseStorage(() => {
didCloseWorkspaceStorage = true;
});
let globalStorage = storageMainService.globalStorage;
let didCloseGlobalStorage = false;
globalStorage.onDidCloseStorage(() => {
didCloseGlobalStorage = true;
});
await globalStorage.close();
await workspaceStorage.close();
strictEqual(didCloseGlobalStorage, true);
strictEqual(didCloseWorkspaceStorage, true);
});
test('storage closed before init awaits works', async function () {
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService());
const workspace = { id: generateUuid() };
let workspaceStorage = storageMainService.workspaceStorage(workspace);
let didCloseWorkspaceStorage = false;
workspaceStorage.onDidCloseStorage(() => {
didCloseWorkspaceStorage = true;
});
let globalStorage = storageMainService.globalStorage;
let didCloseGlobalStorage = false;
globalStorage.onDidCloseStorage(() => {
didCloseGlobalStorage = true;
});
globalStorage.init();
workspaceStorage.init();
await globalStorage.close();
await workspaceStorage.close();
strictEqual(didCloseGlobalStorage, true);
strictEqual(didCloseWorkspaceStorage, true);
});
});

View File

@@ -7,47 +7,56 @@ import { strictEqual } from 'assert';
import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { NativeStorageService } from 'vs/platform/storage/node/storageService';
import { tmpdir } from 'os';
import { mkdirp, rimraf } from 'vs/base/node/pfs';
import { promises } from 'fs';
import { rimraf } from 'vs/base/node/pfs';
import { NullLogService } from 'vs/platform/log/common/log';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { createSuite } from 'vs/platform/storage/test/common/storageService.test';
flakySuite('NativeStorageService', function () {
flakySuite('StorageService (native)', function () {
class StorageTestEnvironmentService extends NativeEnvironmentService {
constructor(private workspaceStorageFolderPath: URI, private _extensionsPath: string) {
super(parseArgs(process.argv, OPTIONS));
}
get workspaceStorageHome(): URI {
return this.workspaceStorageFolderPath;
}
get extensionsPath(): string {
return this._extensionsPath;
}
}
let testDir: string;
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
createSuite<NativeStorageService>({
setup: async () => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
return mkdirp(testDir);
});
await promises.mkdir(testDir, { recursive: true });
teardown(() => {
return rimraf(testDir);
const storageService = new NativeStorageService(new InMemoryStorageDatabase(), { id: String(Date.now()) }, new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir));
await storageService.initialize();
return storageService;
},
teardown: async storageService => {
await storageService.close();
return rimraf(testDir);
}
});
test('Migrate Data', async function () {
class StorageTestEnvironmentService extends NativeEnvironmentService {
constructor(private workspaceStorageFolderPath: URI, private _extensionsPath: string) {
super(parseArgs(process.argv, OPTIONS));
}
get workspaceStorageHome(): URI {
return this.workspaceStorageFolderPath;
}
get extensionsPath(): string {
return this._extensionsPath;
}
}
const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir));
await storage.initialize({ id: String(Date.now()) });
const storage = new NativeStorageService(new InMemoryStorageDatabase(), { id: String(Date.now()) }, new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir));
await storage.initialize();
storage.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE);
storage.store('barNumber', 55, StorageScope.WORKSPACE, StorageTarget.MACHINE);