mirror of
https://github.com/coder/code-server.git
synced 2026-05-10 14:27:26 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
348
lib/vscode/src/vs/platform/storage/browser/storageService.ts
Normal file
348
lib/vscode/src/vs/platform/storage/browser/storageService.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { Emitter } from 'vs/base/common/event';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } 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 { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
|
||||
|
||||
export class BrowserStorageService extends Disposable implements IStorageService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
readonly onDidChangeStorage = this._onDidChangeStorage.event;
|
||||
|
||||
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
|
||||
readonly onWillSaveState = this._onWillSaveState.event;
|
||||
|
||||
private globalStorage: IStorage | undefined;
|
||||
private workspaceStorage: IStorage | undefined;
|
||||
|
||||
private globalStorageDatabase: FileStorageDatabase | undefined;
|
||||
private workspaceStorageDatabase: FileStorageDatabase | undefined;
|
||||
|
||||
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;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IFileService private readonly fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
initialize(payload: IWorkspaceInitializationPayload): Promise<void> {
|
||||
if (!this.initializePromise) {
|
||||
this.initializePromise = this.doInitialize(payload);
|
||||
}
|
||||
|
||||
return this.initializePromise;
|
||||
}
|
||||
|
||||
private async doInitialize(payload: IWorkspaceInitializationPayload): 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.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService));
|
||||
this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase));
|
||||
this._register(this.workspaceStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.WORKSPACE })));
|
||||
|
||||
// Global Storage
|
||||
this.globalStorageFile = joinPath(stateRoot, 'global.json');
|
||||
this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, true /* watch for external changes */, this.fileService));
|
||||
this.globalStorage = this._register(new Storage(this.globalStorageDatabase));
|
||||
this._register(this.globalStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.GLOBAL })));
|
||||
|
||||
// Init both
|
||||
await Promise.all([
|
||||
this.workspaceStorage.init(),
|
||||
this.globalStorage.init()
|
||||
]);
|
||||
|
||||
// Check to see if this is the first time we are "opening" the application
|
||||
const firstOpen = this.globalStorage.getBoolean(IS_NEW_KEY);
|
||||
if (firstOpen === undefined) {
|
||||
this.globalStorage.set(IS_NEW_KEY, true);
|
||||
} else if (firstOpen) {
|
||||
this.globalStorage.set(IS_NEW_KEY, false);
|
||||
}
|
||||
|
||||
// Check to see if this is the first time we are "opening" this workspace
|
||||
const firstWorkspaceOpen = this.workspaceStorage.getBoolean(IS_NEW_KEY);
|
||||
if (firstWorkspaceOpen === undefined) {
|
||||
this.workspaceStorage.set(IS_NEW_KEY, true);
|
||||
} else if (firstWorkspaceOpen) {
|
||||
this.workspaceStorage.set(IS_NEW_KEY, false);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void {
|
||||
this.getStorage(scope).set(key, value);
|
||||
}
|
||||
|
||||
remove(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());
|
||||
}
|
||||
|
||||
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
|
||||
throw new Error('Migrating storage is currently unsupported in Web');
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
get hasPendingUpdate(): boolean {
|
||||
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
|
||||
}
|
||||
|
||||
close(): void {
|
||||
// We explicitly do not close our DBs because writing data onBeforeUnload()
|
||||
// can result in unexpected results. Namely, it seems that - even though this
|
||||
// operation is async - sometimes it is being triggered on unload and
|
||||
// succeeds. Often though, the DBs turn out to be empty because the write
|
||||
// never had a chance to complete.
|
||||
//
|
||||
// Instead we trigger dispose() to ensure that no timeouts or callbacks
|
||||
// get triggered in this phase.
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
isNew(scope: StorageScope): boolean {
|
||||
return this.getBoolean(IS_NEW_KEY, scope) === true;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this.runWhenIdleDisposable);
|
||||
this.runWhenIdleDisposable = undefined;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class FileStorageDatabase extends Disposable implements IStorageDatabase {
|
||||
|
||||
private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>());
|
||||
readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event;
|
||||
|
||||
private cache: Map<string, string> | undefined;
|
||||
|
||||
private pendingUpdate: Promise<void> = Promise.resolve();
|
||||
|
||||
private _hasPendingUpdate = false;
|
||||
get hasPendingUpdate(): boolean {
|
||||
return this._hasPendingUpdate;
|
||||
}
|
||||
|
||||
private isWatching = false;
|
||||
|
||||
constructor(
|
||||
private readonly file: URI,
|
||||
private readonly watchForExternalChanges: boolean,
|
||||
@IFileService private readonly fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private async ensureWatching(): Promise<void> {
|
||||
if (this.isWatching || !this.watchForExternalChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = await this.fileService.exists(this.file);
|
||||
if (this.isWatching || !exists) {
|
||||
return; // file must exist to be watched
|
||||
}
|
||||
|
||||
this.isWatching = true;
|
||||
|
||||
this._register(this.fileService.watch(this.file));
|
||||
this._register(this.fileService.onDidFilesChange(e => {
|
||||
if (document.hasFocus()) {
|
||||
return; // optimization: ignore changes from ourselves by checking for focus
|
||||
}
|
||||
|
||||
if (!e.contains(this.file, FileChangeType.UPDATED)) {
|
||||
return; // not our file
|
||||
}
|
||||
|
||||
this.onDidStorageChangeExternal();
|
||||
}));
|
||||
}
|
||||
|
||||
private async onDidStorageChangeExternal(): Promise<void> {
|
||||
const items = await this.doGetItemsFromFile();
|
||||
|
||||
// pervious cache, diff for changes
|
||||
let changed = new Map<string, string>();
|
||||
let deleted = new Set<string>();
|
||||
if (this.cache) {
|
||||
items.forEach((value, key) => {
|
||||
const existingValue = this.cache?.get(key);
|
||||
if (existingValue !== value) {
|
||||
changed.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
this.cache.forEach((_, key) => {
|
||||
if (!items.has(key)) {
|
||||
deleted.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// no previous cache, consider all as changed
|
||||
else {
|
||||
changed = items;
|
||||
}
|
||||
|
||||
// Update cache
|
||||
this.cache = items;
|
||||
|
||||
// Emit as event as needed
|
||||
if (changed.size > 0 || deleted.size > 0) {
|
||||
this._onDidChangeItemsExternal.fire({ changed, deleted });
|
||||
}
|
||||
}
|
||||
|
||||
async getItems(): Promise<Map<string, string>> {
|
||||
if (!this.cache) {
|
||||
try {
|
||||
this.cache = await this.doGetItemsFromFile();
|
||||
} catch (error) {
|
||||
this.cache = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
private async doGetItemsFromFile(): Promise<Map<string, string>> {
|
||||
await this.pendingUpdate;
|
||||
|
||||
const itemsRaw = await this.fileService.readFile(this.file);
|
||||
|
||||
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
|
||||
|
||||
return new Map(JSON.parse(itemsRaw.value.toString()));
|
||||
}
|
||||
|
||||
async updateItems(request: IUpdateRequest): Promise<void> {
|
||||
const items = await this.getItems();
|
||||
|
||||
if (request.insert) {
|
||||
request.insert.forEach((value, key) => items.set(key, value));
|
||||
}
|
||||
|
||||
if (request.delete) {
|
||||
request.delete.forEach(key => items.delete(key));
|
||||
}
|
||||
|
||||
await this.pendingUpdate;
|
||||
|
||||
this.pendingUpdate = (async () => {
|
||||
try {
|
||||
this._hasPendingUpdate = true;
|
||||
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(Array.from(items.entries()))));
|
||||
|
||||
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
|
||||
} finally {
|
||||
this._hasPendingUpdate = false;
|
||||
}
|
||||
})();
|
||||
|
||||
return this.pendingUpdate;
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return this.pendingUpdate;
|
||||
}
|
||||
}
|
||||
289
lib/vscode/src/vs/platform/storage/common/storage.ts
Normal file
289
lib/vscode/src/vs/platform/storage/common/storage.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
|
||||
|
||||
export const IS_NEW_KEY = '__$__isNewStorageMarker';
|
||||
|
||||
export const IStorageService = createDecorator<IStorageService>('storageService');
|
||||
|
||||
export enum WillSaveStateReason {
|
||||
NONE = 0,
|
||||
SHUTDOWN = 1
|
||||
}
|
||||
|
||||
export interface IWillSaveStateEvent {
|
||||
reason: WillSaveStateReason;
|
||||
}
|
||||
|
||||
export interface IStorageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Emitted whenever data is updated or deleted.
|
||||
*/
|
||||
readonly onDidChangeStorage: Event<IWorkspaceStorageChangeEvent>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The will save state event allows to optionally ask for the reason of
|
||||
* saving the state, e.g. to find out if the state is saved due to a
|
||||
* shutdown.
|
||||
*
|
||||
* 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<IWillSaveStateEvent>;
|
||||
|
||||
/**
|
||||
* Retrieve an element stored with the given key from storage. Use
|
||||
* the provided defaultValue if the element is null or undefined.
|
||||
*
|
||||
* The scope argument allows to define the scope of the storage
|
||||
* operation to either the current workspace only or all workspaces.
|
||||
*/
|
||||
get(key: string, scope: StorageScope, fallbackValue: string): string;
|
||||
get(key: string, scope: StorageScope, 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.
|
||||
*
|
||||
* The scope argument allows to define the scope of the storage
|
||||
* operation to either the current workspace only or all workspaces.
|
||||
*/
|
||||
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, scope: StorageScope, 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.
|
||||
*
|
||||
* The scope argument allows to define the scope of the storage
|
||||
* operation to either the current workspace only or all workspaces.
|
||||
*/
|
||||
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
|
||||
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
|
||||
|
||||
/**
|
||||
* Store a value under the given key to storage. The value will be converted to a string.
|
||||
* Storing either undefined or null will remove the entry under the key.
|
||||
*
|
||||
* The scope argument allows to define the scope of the storage
|
||||
* operation to either the current workspace only or all workspaces.
|
||||
*/
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void;
|
||||
|
||||
/**
|
||||
* Delete an element stored under the provided key from storage.
|
||||
*
|
||||
* The scope argument allows to define the scope of the storage
|
||||
* operation to either the current workspace only or all workspaces.
|
||||
*/
|
||||
remove(key: string, scope: StorageScope): void;
|
||||
|
||||
/**
|
||||
* Log the contents of the storage to the console.
|
||||
*/
|
||||
logStorage(): void;
|
||||
|
||||
/**
|
||||
* Migrate the storage contents to another workspace.
|
||||
*/
|
||||
migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void>;
|
||||
|
||||
/**
|
||||
* Whether the storage for the given scope was created during this session or
|
||||
* existed before.
|
||||
*
|
||||
*/
|
||||
isNew(scope: StorageScope): boolean;
|
||||
|
||||
/**
|
||||
* Allows to flush state, e.g. in cases where a shutdown is
|
||||
* imminent. This will send out the onWillSaveState to ask
|
||||
* everyone for latest state.
|
||||
*/
|
||||
flush(): void;
|
||||
}
|
||||
|
||||
export const enum StorageScope {
|
||||
|
||||
/**
|
||||
* The stored data will be scoped to all workspaces.
|
||||
*/
|
||||
GLOBAL,
|
||||
|
||||
/**
|
||||
* The stored data will be scoped to the current workspace.
|
||||
*/
|
||||
WORKSPACE
|
||||
}
|
||||
|
||||
export interface IWorkspaceStorageChangeEvent {
|
||||
readonly key: string;
|
||||
readonly scope: StorageScope;
|
||||
}
|
||||
|
||||
export class InMemoryStorageService extends Disposable implements IStorageService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
readonly onDidChangeStorage = this._onDidChangeStorage.event;
|
||||
|
||||
protected readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
|
||||
readonly onWillSaveState = this._onWillSaveState.event;
|
||||
|
||||
private readonly globalCache = new Map<string, string>();
|
||||
private readonly workspaceCache = new Map<string, string>();
|
||||
|
||||
private getCache(scope: StorageScope): Map<string, string> {
|
||||
return scope === StorageScope.GLOBAL ? this.globalCache : this.workspaceCache;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
|
||||
|
||||
// We remove the key for undefined/null values
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return this.remove(key, scope);
|
||||
}
|
||||
|
||||
// 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 Promise.resolve();
|
||||
}
|
||||
|
||||
// Update in cache
|
||||
this.getCache(scope).set(key, valueStr);
|
||||
|
||||
// Events
|
||||
this._onDidChangeStorage.fire({ scope, key });
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
remove(key: string, scope: StorageScope): Promise<void> {
|
||||
const wasDeleted = this.getCache(scope).delete(key);
|
||||
if (!wasDeleted) {
|
||||
return Promise.resolve(); // Return early if value already deleted
|
||||
}
|
||||
|
||||
// Events
|
||||
this._onDidChangeStorage.fire({ scope, key });
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
logStorage(): void {
|
||||
logStorage(this.globalCache, this.workspaceCache, 'inMemory', 'inMemory');
|
||||
}
|
||||
|
||||
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
|
||||
// not supported
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
|
||||
}
|
||||
|
||||
isNew(): boolean {
|
||||
return true; // always new when in-memory
|
||||
}
|
||||
|
||||
async close(): Promise<void> { }
|
||||
}
|
||||
|
||||
export async function logStorage(global: Map<string, string>, workspace: Map<string, string>, globalPath: string, workspacePath: string): Promise<void> {
|
||||
const safeParse = (value: string) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const globalItems = new Map<string, string>();
|
||||
const globalItemsParsed = new Map<string, string>();
|
||||
global.forEach((value, key) => {
|
||||
globalItems.set(key, value);
|
||||
globalItemsParsed.set(key, safeParse(value));
|
||||
});
|
||||
|
||||
const workspaceItems = new Map<string, string>();
|
||||
const workspaceItemsParsed = new Map<string, string>();
|
||||
workspace.forEach((value, key) => {
|
||||
workspaceItems.set(key, value);
|
||||
workspaceItemsParsed.set(key, safeParse(value));
|
||||
});
|
||||
|
||||
console.group(`Storage: Global (path: ${globalPath})`);
|
||||
let globalValues: { key: string, value: string }[] = [];
|
||||
globalItems.forEach((value, key) => {
|
||||
globalValues.push({ key, value });
|
||||
});
|
||||
console.table(globalValues);
|
||||
console.groupEnd();
|
||||
|
||||
console.log(globalItemsParsed);
|
||||
|
||||
console.group(`Storage: Workspace (path: ${workspacePath})`);
|
||||
let workspaceValues: { key: string, value: string }[] = [];
|
||||
workspaceItems.forEach((value, key) => {
|
||||
workspaceValues.push({ key, value });
|
||||
});
|
||||
console.table(workspaceValues);
|
||||
console.groupEnd();
|
||||
|
||||
console.log(workspaceItemsParsed);
|
||||
}
|
||||
216
lib/vscode/src/vs/platform/storage/node/storageIpc.ts
Normal file
216
lib/vscode/src/vs/platform/storage/node/storageIpc.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
|
||||
// when we are about to close, we start to ignore main-side changes since we close anyway
|
||||
dispose(this.onDidChangeItemsOnMainListener);
|
||||
|
||||
return Promise.resolve(); // global storage is closed on the main side
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
dispose(this.onDidChangeItemsOnMainListener);
|
||||
}
|
||||
}
|
||||
191
lib/vscode/src/vs/platform/storage/node/storageMainService.ts
Normal file
191
lib/vscode/src/vs/platform/storage/node/storageMainService.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
284
lib/vscode/src/vs/platform/storage/node/storageService.ts
Normal file
284
lib/vscode/src/vs/platform/storage/node/storageService.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { Emitter } from 'vs/base/common/event';
|
||||
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } 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 { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
|
||||
|
||||
export class NativeStorageService extends Disposable implements IStorageService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb';
|
||||
private static readonly WORKSPACE_META_NAME = 'workspace.json';
|
||||
|
||||
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
readonly onDidChangeStorage = this._onDidChangeStorage.event;
|
||||
|
||||
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
|
||||
readonly onWillSaveState = this._onWillSaveState.event;
|
||||
|
||||
private readonly globalStorage = new Storage(this.globalStorageDatabase);
|
||||
|
||||
private workspaceStoragePath: string | undefined;
|
||||
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,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Global Storage change events
|
||||
this._register(this.globalStorage.onDidChangeStorage(key => this.handleDidChangeStorage(key, StorageScope.GLOBAL)));
|
||||
}
|
||||
|
||||
private handleDidChangeStorage(key: string, scope: StorageScope): void {
|
||||
this._onDidChangeStorage.fire({ key, scope });
|
||||
}
|
||||
|
||||
initialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
|
||||
if (!this.initializePromise) {
|
||||
this.initializePromise = this.doInitialize(payload);
|
||||
}
|
||||
|
||||
return this.initializePromise;
|
||||
}
|
||||
|
||||
private async doInitialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
|
||||
|
||||
// Init all storage locations
|
||||
await Promise.all([
|
||||
this.initializeGlobalStorage(),
|
||||
payload ? this.initializeWorkspaceStorage(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> {
|
||||
return this.globalStorage.init();
|
||||
}
|
||||
|
||||
private async initializeWorkspaceStorage(payload: IWorkspaceInitializationPayload): Promise<void> {
|
||||
|
||||
// Prepare workspace storage folder for DB
|
||||
try {
|
||||
const result = await this.prepareWorkspaceStorageFolder(payload);
|
||||
|
||||
const useInMemoryStorage = !!this.environmentService.extensionTestsLocationURI; // no storage during extension tests!
|
||||
|
||||
// Create workspace storage and initialize
|
||||
mark('willInitWorkspaceStorage');
|
||||
try {
|
||||
const workspaceStorage = this.createWorkspaceStorage(
|
||||
useInMemoryStorage ? SQLiteStorageDatabase.IN_MEMORY_PATH : join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME),
|
||||
result.wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined
|
||||
);
|
||||
await workspaceStorage.init();
|
||||
|
||||
// Check to see if this is the first time we are "opening" this workspace
|
||||
const firstWorkspaceOpen = workspaceStorage.getBoolean(IS_NEW_KEY);
|
||||
if (firstWorkspaceOpen === undefined) {
|
||||
workspaceStorage.set(IS_NEW_KEY, result.wasCreated);
|
||||
} else if (firstWorkspaceOpen) {
|
||||
workspaceStorage.set(IS_NEW_KEY, false);
|
||||
}
|
||||
} finally {
|
||||
mark('didInitWorkspaceStorage');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(`[storage] initializeWorkspaceStorage(): Unable to init workspace storage due to ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private createWorkspaceStorage(workspaceStoragePath: string, hint?: StorageHint): IStorage {
|
||||
|
||||
// Logger for workspace storage
|
||||
const workspaceLoggingOptions: ISQLiteStorageDatabaseLoggingOptions = {
|
||||
logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined,
|
||||
logError: error => this.logService.error(error)
|
||||
};
|
||||
|
||||
// Dispose old (if any)
|
||||
dispose(this.workspaceStorage);
|
||||
dispose(this.workspaceStorageListener);
|
||||
|
||||
// Create new
|
||||
this.workspaceStoragePath = workspaceStoragePath;
|
||||
this.workspaceStorage = new Storage(new SQLiteStorageDatabase(workspaceStoragePath, { logging: workspaceLoggingOptions }), { hint });
|
||||
this.workspaceStorageListener = this.workspaceStorage.onDidChangeStorage(key => this.handleDidChangeStorage(key, StorageScope.WORKSPACE));
|
||||
|
||||
return this.workspaceStorage;
|
||||
}
|
||||
|
||||
private getWorkspaceStorageFolderPath(payload: IWorkspaceInitializationPayload): string {
|
||||
return join(this.environmentService.workspaceStorageHome.fsPath, payload.id); // workspace home + workspace id;
|
||||
}
|
||||
|
||||
private async prepareWorkspaceStorageFolder(payload: IWorkspaceInitializationPayload): Promise<{ path: string, wasCreated: boolean }> {
|
||||
const workspaceStorageFolderPath = this.getWorkspaceStorageFolderPath(payload);
|
||||
|
||||
const storageExists = await exists(workspaceStorageFolderPath);
|
||||
if (storageExists) {
|
||||
return { path: workspaceStorageFolderPath, wasCreated: false };
|
||||
}
|
||||
|
||||
await mkdirp(workspaceStorageFolderPath);
|
||||
|
||||
// Write metadata into folder
|
||||
this.ensureWorkspaceStorageFolderMeta(payload);
|
||||
|
||||
return { path: workspaceStorageFolderPath, wasCreated: true };
|
||||
}
|
||||
|
||||
private ensureWorkspaceStorageFolderMeta(payload: IWorkspaceInitializationPayload): void {
|
||||
let meta: object | undefined = undefined;
|
||||
if (isSingleFolderWorkspaceInitializationPayload(payload)) {
|
||||
meta = { folder: payload.folder.toString() };
|
||||
} else if (isWorkspaceIdentifier(payload)) {
|
||||
meta = { configuration: payload.configPath };
|
||||
}
|
||||
|
||||
if (meta) {
|
||||
const logService = this.logService;
|
||||
const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME);
|
||||
(async function () {
|
||||
try {
|
||||
const storageExists = await exists(workspaceStorageMetaPath);
|
||||
if (!storageExists) {
|
||||
await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2));
|
||||
}
|
||||
} catch (error) {
|
||||
logService.error(error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
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): void {
|
||||
this.getStorage(scope).set(key, value);
|
||||
}
|
||||
|
||||
remove(key: string, scope: StorageScope): void {
|
||||
this.getStorage(scope).delete(key);
|
||||
}
|
||||
|
||||
private getStorage(scope: StorageScope): IStorage {
|
||||
return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Signal as event so that clients can still store data
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
|
||||
|
||||
// Do it
|
||||
await Promise.all([
|
||||
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();
|
||||
|
||||
// Prepare new workspace storage folder
|
||||
const result = await this.prepareWorkspaceStorageFolder(toWorkspace);
|
||||
|
||||
const newWorkspaceStoragePath = join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME);
|
||||
|
||||
// Copy current storage over to new workspace storage
|
||||
await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath);
|
||||
|
||||
// Recreate and init workspace storage
|
||||
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();
|
||||
}
|
||||
|
||||
isNew(scope: StorageScope): boolean {
|
||||
return this.getBoolean(IS_NEW_KEY, scope) === true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { equal } from 'assert';
|
||||
import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { tmpdir } from 'os';
|
||||
import { rimraf, RimRafMode } from 'vs/base/node/pfs';
|
||||
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';
|
||||
|
||||
suite('Storage', () => {
|
||||
|
||||
const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
|
||||
|
||||
let fileService: FileService;
|
||||
let fileProvider: DiskFileSystemProvider;
|
||||
let testDir: string;
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
setup(async () => {
|
||||
const logService = new NullLogService();
|
||||
|
||||
fileService = new FileService(logService);
|
||||
disposables.add(fileService);
|
||||
|
||||
fileProvider = new DiskFileSystemProvider(logService);
|
||||
disposables.add(fileService.registerProvider(Schemas.file, fileProvider));
|
||||
disposables.add(fileProvider);
|
||||
|
||||
const id = generateUuid();
|
||||
testDir = join(parentDir, id);
|
||||
});
|
||||
|
||||
teardown(async () => {
|
||||
disposables.clear();
|
||||
|
||||
await rimraf(parentDir, RimRafMode.MOVE);
|
||||
});
|
||||
|
||||
test('File Based Storage', async () => {
|
||||
let storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
|
||||
|
||||
await storage.init();
|
||||
|
||||
storage.set('bar', 'foo');
|
||||
storage.set('barNumber', 55);
|
||||
storage.set('barBoolean', true);
|
||||
|
||||
equal(storage.get('bar'), 'foo');
|
||||
equal(storage.get('barNumber'), '55');
|
||||
equal(storage.get('barBoolean'), 'true');
|
||||
|
||||
await storage.close();
|
||||
|
||||
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
|
||||
|
||||
await storage.init();
|
||||
|
||||
equal(storage.get('bar'), 'foo');
|
||||
equal(storage.get('barNumber'), '55');
|
||||
equal(storage.get('barBoolean'), 'true');
|
||||
|
||||
storage.delete('bar');
|
||||
storage.delete('barNumber');
|
||||
storage.delete('barBoolean');
|
||||
|
||||
equal(storage.get('bar', 'undefined'), 'undefined');
|
||||
equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber');
|
||||
equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean');
|
||||
|
||||
await storage.close();
|
||||
|
||||
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
|
||||
|
||||
await storage.init();
|
||||
|
||||
equal(storage.get('bar', 'undefined'), 'undefined');
|
||||
equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber');
|
||||
equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean');
|
||||
});
|
||||
});
|
||||
@@ -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 { strictEqual, ok, equal } from 'assert';
|
||||
import { StorageScope, InMemoryStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { NativeStorageService } from 'vs/platform/storage/node/storageService';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { tmpdir } from 'os';
|
||||
import { mkdirp, rimraf, RimRafMode } 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';
|
||||
|
||||
suite('StorageService', function () {
|
||||
|
||||
test('Remove Data (global, in-memory)', () => {
|
||||
removeData(StorageScope.GLOBAL);
|
||||
});
|
||||
|
||||
test('Remove Data (workspace, in-memory)', () => {
|
||||
removeData(StorageScope.WORKSPACE);
|
||||
});
|
||||
|
||||
function removeData(scope: StorageScope): void {
|
||||
const storage = new InMemoryStorageService();
|
||||
|
||||
storage.store('test.remove', 'foobar', scope);
|
||||
strictEqual('foobar', storage.get('test.remove', scope, (undefined)!));
|
||||
|
||||
storage.remove('test.remove', scope);
|
||||
ok(!storage.get('test.remove', scope, (undefined)!));
|
||||
}
|
||||
|
||||
test('Get Data, Integer, Boolean (global, in-memory)', () => {
|
||||
storeData(StorageScope.GLOBAL);
|
||||
});
|
||||
|
||||
test('Get Data, Integer, Boolean (workspace, in-memory)', () => {
|
||||
storeData(StorageScope.WORKSPACE);
|
||||
});
|
||||
|
||||
function storeData(scope: StorageScope): void {
|
||||
const storage = new InMemoryStorageService();
|
||||
|
||||
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);
|
||||
|
||||
storage.store('test.get', 'foobar', scope);
|
||||
strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar');
|
||||
|
||||
storage.store('test.get', '', scope);
|
||||
strictEqual(storage.get('test.get', scope, (undefined)!), '');
|
||||
|
||||
storage.store('test.getNumber', 5, scope);
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5);
|
||||
|
||||
storage.store('test.getNumber', 0, scope);
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0);
|
||||
|
||||
storage.store('test.getBoolean', true, scope);
|
||||
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true);
|
||||
|
||||
storage.store('test.getBoolean', false, scope);
|
||||
strictEqual(storage.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);
|
||||
}
|
||||
|
||||
function uniqueStorageDir(): string {
|
||||
const id = generateUuid();
|
||||
|
||||
return join(tmpdir(), 'vsctests', id, 'storage2', id);
|
||||
}
|
||||
|
||||
test('Migrate Data', async () => {
|
||||
|
||||
// Given issues such as https://github.com/microsoft/vscode/issues/108113
|
||||
// we see random test failures when accessing the native file system.
|
||||
this.retries(3);
|
||||
this.timeout(1000 * 20);
|
||||
|
||||
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 storageDir = uniqueStorageDir();
|
||||
await mkdirp(storageDir);
|
||||
|
||||
const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(storageDir), storageDir));
|
||||
await storage.initialize({ id: String(Date.now()) });
|
||||
|
||||
storage.store('bar', 'foo', StorageScope.WORKSPACE);
|
||||
storage.store('barNumber', 55, StorageScope.WORKSPACE);
|
||||
storage.store('barBoolean', true, StorageScope.GLOBAL);
|
||||
|
||||
await storage.migrate({ id: String(Date.now() + 100) });
|
||||
|
||||
equal(storage.get('bar', StorageScope.WORKSPACE), 'foo');
|
||||
equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55);
|
||||
equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
|
||||
|
||||
await storage.close();
|
||||
await rimraf(storageDir, RimRafMode.MOVE);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user