UNPKG

sussudio

Version:

An unofficial VS Code Internal API

266 lines (265 loc) 12.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { top } from "../../../base/common/arrays.mjs"; import { DeferredPromise } from "../../../base/common/async.mjs"; import { Emitter } from "../../../base/common/event.mjs"; import { Disposable } from "../../../base/common/lifecycle.mjs"; import { join } from "../../../base/common/path.mjs"; import { StopWatch } from "../../../base/common/stopwatch.mjs"; import { URI } from "../../../base/common/uri.mjs"; import { Promises } from "../../../base/node/pfs.mjs"; import { InMemoryStorageDatabase, Storage, StorageHint, StorageState } from "../../../base/parts/storage/common/storage.mjs"; import { SQLiteStorageDatabase } from "../../../base/parts/storage/node/storage.mjs"; import { LogLevel } from "../../log/common/log.mjs"; import { IS_NEW_KEY } from "../common/storage.mjs"; import { currentSessionDateStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey } from "../../telemetry/common/telemetry.mjs"; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from "../../workspace/common/workspace.mjs"; class BaseStorageMain extends Disposable { logService; fileService; static LOG_SLOW_CLOSE_THRESHOLD = 2000; _onDidChangeStorage = this._register(new Emitter()); onDidChangeStorage = this._onDidChangeStorage.event; _onDidCloseStorage = this._register(new Emitter()); onDidCloseStorage = this._onDidCloseStorage.event; _storage = new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }); // storage is in-memory until initialized get storage() { return this._storage; } initializePromise = undefined; whenInitPromise = new DeferredPromise(); whenInit = this.whenInitPromise.p; state = StorageState.None; constructor(logService, fileService) { super(); this.logService = logService; this.fileService = fileService; } isInMemory() { return this._storage.isInMemory(); } init() { if (!this.initializePromise) { this.initializePromise = (async () => { if (this.state !== StorageState.None) { return; // either closed or already initialized } 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(`[storage main] initialize(): Unable to init storage due to ${error}`); } finally { // Update state this.state = StorageState.Initialized; // Mark init promise as completed this.whenInitPromise.complete(); } })(); } return this.initializePromise; } createLoggingOptions() { return { logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined, logError: error => this.logService.error(error) }; } doInit(storage) { return storage.init(); } get items() { return this._storage.items; } get(key, fallbackValue) { return this._storage.get(key, fallbackValue); } set(key, value) { return this._storage.set(key, value); } delete(key) { return this._storage.delete(key); } async close() { // Measure how long it takes to close storage const watch = new StopWatch(false); await this.doClose(); watch.stop(); // If close() is taking a long time, there is // a chance that the underlying DB is large // either on disk or in general. In that case // log some additional info to further diagnose if (watch.elapsed() > BaseStorageMain.LOG_SLOW_CLOSE_THRESHOLD) { await this.logSlowClose(watch); } // Signal as event this._onDidCloseStorage.fire(); } async logSlowClose(watch) { if (!this.path) { return; } try { const largestEntries = top(Array.from(this._storage.items.entries()) .map(([key, value]) => ({ key, length: value.length })), (entryA, entryB) => entryB.length - entryA.length, 5) .map(entry => `${entry.key}:${entry.length}`).join(', '); const dbSize = (await this.fileService.stat(URI.file(this.path))).size; this.logService.warn(`[storage main] detected slow close() operation: Time: ${watch.elapsed()}ms, DB size: ${dbSize}b, Large Keys: ${largestEntries}`); } catch (error) { this.logService.error('[storage main] figuring out stats for slow DB on close() resulted in an error', error); } } async doClose() { // 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; } // Update state this.state = StorageState.Closed; // Propagate to storage lib await this._storage.close(); } } class BaseProfileAwareStorageMain extends BaseStorageMain { profile; options; static STORAGE_NAME = 'state.vscdb'; get path() { if (!this.options.useInMemoryStorage) { return join(this.profile.globalStorageHome.fsPath, BaseProfileAwareStorageMain.STORAGE_NAME); } return undefined; } constructor(profile, options, logService, fileService) { super(logService, fileService); this.profile = profile; this.options = options; } async doCreate() { return new Storage(new SQLiteStorageDatabase(this.path ?? SQLiteStorageDatabase.IN_MEMORY_PATH, { logging: this.createLoggingOptions() }), !this.path ? { hint: StorageHint.STORAGE_IN_MEMORY } : undefined); } } export class ProfileStorageMain extends BaseProfileAwareStorageMain { constructor(profile, options, logService, fileService) { super(profile, options, logService, fileService); } } export class ApplicationStorageMain extends BaseProfileAwareStorageMain { constructor(options, userDataProfileService, logService, fileService) { super(userDataProfileService.defaultProfile, options, logService, fileService); } async doInit(storage) { await super.doInit(storage); // Apply telemetry values as part of the application storage initialization this.updateTelemetryState(storage); } updateTelemetryState(storage) { // 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 { workspace; options; environmentService; static WORKSPACE_STORAGE_NAME = 'state.vscdb'; static WORKSPACE_META_NAME = 'workspace.json'; get path() { if (!this.options.useInMemoryStorage) { return join(this.environmentService.workspaceStorageHome.fsPath, this.workspace.id, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME); } return undefined; } constructor(workspace, options, logService, environmentService, fileService) { super(logService, fileService); this.workspace = workspace; this.options = options; this.environmentService = environmentService; } async doCreate() { const { storageFilePath, wasCreated } = await this.prepareWorkspaceStorageFolder(); return new Storage(new SQLiteStorageDatabase(storageFilePath, { logging: this.createLoggingOptions() }), { hint: this.options.useInMemoryStorage ? StorageHint.STORAGE_IN_MEMORY : wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined }); } async prepareWorkspaceStorageFolder() { // 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 Promises.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 }; } async ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath) { let meta = 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 Promises.exists(workspaceStorageMetaPath); if (!storageExists) { await Promises.writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); } } catch (error) { this.logService.error(`[storage main] ensureWorkspaceStorageFolderMeta(): Unable to create workspace storage metadata due to ${error}`); } } } } export class InMemoryStorageMain extends BaseStorageMain { get path() { return undefined; // in-memory has no path } async doCreate() { return new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }); } }