mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-29 09:43:49 +01:00
286 lines
11 KiB
TypeScript
286 lines
11 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
|
|
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: INativeEnvironmentService
|
|
) {
|
|
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;
|
|
}
|
|
}
|