diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 1e4f3c62849..0e4264134c1 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -58,8 +58,8 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService'; -import { NativeStorageService } from 'vs/platform/storage/node/storageService'; -import { GlobalStorageDatabaseChannelClient } from 'vs/platform/storage/node/storageIpc'; +import { NativeStorageService2 } from 'vs/platform/storage/node/storageService2'; +import { StorageDatabaseChannelClient } from 'vs/platform/storage/node/storageIpc'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; @@ -173,7 +173,8 @@ class SharedProcessMain extends Disposable { await configurationService.initialize(); // Storage - const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService); + const storageDatabase = new StorageDatabaseChannelClient(mainProcessService.getChannel('storage'), undefined /* no workspace access for shared process */); + const storageService = new NativeStorageService2(storageDatabase.globalStorage, storageDatabase.workspaceStorage, environmentService); services.set(IStorageService, storageService); await storageService.initialize(); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index ef4909c7d6b..9646be97a2b 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -59,7 +59,7 @@ import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap'; import { IStorageMainService, StorageMainService } from 'vs/platform/storage/node/storageMainService'; -import { GlobalStorageDatabaseChannel } from 'vs/platform/storage/node/storageIpc'; +import { StorageDatabaseChannel } from 'vs/platform/storage/node/storageIpc'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; @@ -582,7 +582,7 @@ export class CodeApplication extends Disposable { // Storage const storageMainService = new StorageMainService(this.logService, this.environmentService); services.set(IStorageMainService, storageMainService); - this.lifecycleMainService.onWillShutdown(e => e.join(storageMainService.close())); + this.lifecycleMainService.onWillShutdown(e => e.join(storageMainService.globalStorage.close())); // Backups const backupMainService = new BackupMainService(this.environmentService, this.configurationService, this.logService); @@ -666,7 +666,7 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('webview', webviewChannel); // Storage (main & shared process) - const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, accessor.get(IStorageMainService))); + const storageChannel = this._register(new StorageDatabaseChannel(this.logService, accessor.get(IStorageMainService))); mainProcessElectronServer.registerChannel('storage', storageChannel); sharedProcessClient.then(client => client.registerChannel('storage', storageChannel)); diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 67195995c28..e4a95707a57 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -3,20 +3,24 @@ * 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, dispose, IDisposable } from 'vs/base/common/lifecycle'; 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 { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; 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'; +import { IStorageChangeEvent, IStorageMain } from 'vs/platform/storage/node/storageMain'; +import { IStorageMainService } from 'vs/platform/storage/node/storageMainService'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; type Key = string; type Value = string; type Item = [Key, Value]; -interface ISerializableUpdateRequest { +interface IWorkspaceArgument { + workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined +} + +interface ISerializableUpdateRequest extends IWorkspaceArgument { insert?: Item[]; delete?: Key[]; } @@ -26,60 +30,34 @@ interface ISerializableItemsChangeEvent { readonly deleted?: Key[]; } -export class GlobalStorageDatabaseChannel extends Disposable implements IServerChannel { +//#region --- Storage Server + +export class StorageDatabaseChannel extends Disposable implements IServerChannel { private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100; - private readonly _onDidChangeItems = this._register(new Emitter()); - readonly onDidChangeItems = this._onDidChangeItems.event; - - private readonly whenReady = this.init(); + private readonly _onDidChangeGlobalStorage = this._register(new Emitter()); + private readonly onDidChangeGlobalStorage = this._onDidChangeGlobalStorage.event; constructor( private logService: ILogService, private storageMainService: IStorageMainService ) { super(); + + // Trigger init of global storage directly from ctor + this.withStorageInitialized(undefined); + + this.registerGlobalStorageListeners(); } - private async init(): Promise { - try { - await this.storageMainService.initialize(); - } catch (error) { - this.logService.error(`[storage] init(): Unable to init global storage due to ${error}`); - } + //#region Global Storage Change Events - // 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 { + 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.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => { + this._register(Event.debounce(this.storageMainService.globalStorage.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => { if (!prev) { prev = [cur]; } else { @@ -87,18 +65,18 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC } return prev; - }, GlobalStorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => { + }, StorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => { if (events.length) { - this._onDidChangeItems.fire(this.serializeEvents(events)); + this._onDidChangeGlobalStorage.fire(this.serializeGlobalStorageEvents(events)); } })); } - private serializeEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent { + private serializeGlobalStorageEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent { const changed = new Map(); const deleted = new Set(); events.forEach(event => { - const existing = this.storageMainService.get(event.key); + const existing = this.storageMainService.globalStorage.get(event.key); if (typeof existing === 'string') { changed.set(event.key, existing); } else { @@ -114,33 +92,35 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC listen(_: unknown, event: string): Event { switch (event) { - case 'onDidChangeItems': return this.onDidChangeItems; + case 'onDidChangeGlobalStorage': return this.onDidChangeGlobalStorage; } throw new Error(`Event not found: ${event}`); } - async call(_: unknown, command: string, arg?: any): Promise { + //#endregion - // ensure to always wait for ready - await this.whenReady; + async call(_: unknown, command: string, arg: IWorkspaceArgument): Promise { + + // Get storage to be ready + const storage = await this.withStorageInitialized(arg.workspace); // handle call switch (command) { case 'getItems': { - return Array.from(this.storageMainService.items.entries()); + return Array.from(storage.items.entries()); } case 'updateItems': { const items: ISerializableUpdateRequest = arg; if (items.insert) { for (const [key, value] of items.insert) { - this.storageMainService.store(key, value); + storage.store(key, value); } } if (items.delete) { - items.delete.forEach(key => this.storageMainService.remove(key)); + items.delete.forEach(key => storage.remove(key)); } break; @@ -150,44 +130,41 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC throw new Error(`Call not found: ${command}`); } } + + private async withStorageInitialized(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined): Promise { + const storage = workspace ? this.storageMainService.workspaceStorage(workspace) : this.storageMainService.globalStorage; + + try { + await storage.initialize(); + } catch (error) { + this.logService.error(`[storage] init(): Unable to init ${workspace ? 'workspace' : 'global'} storage due to ${error}`); + } + + return storage; + } } -export class GlobalStorageDatabaseChannelClient extends Disposable implements IStorageDatabase { +//#endregion - declare readonly _serviceBrand: undefined; +//#region --- Storage Client - private readonly _onDidChangeItemsExternal = this._register(new Emitter()); - readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; +abstract class BaseStorageDatabase extends Disposable implements IStorageDatabase { - private onDidChangeItemsOnMainListener: IDisposable | undefined; + abstract onDidChangeItemsExternal: Event; - constructor(private channel: IChannel) { + constructor(protected channel: IChannel, private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined) { super(); - - this.registerListeners(); - } - - private registerListeners(): void { - this.onDidChangeItemsOnMainListener = this.channel.listen('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(e.deleted) : undefined - }); - } } async getItems(): Promise> { - const items: Item[] = await this.channel.call('getItems'); + const serializableRequest: IWorkspaceArgument = { workspace: this.workspace }; + const items: Item[] = await this.channel.call('getItems', serializableRequest); return new Map(items); } updateItems(request: IUpdateRequest): Promise { - const serializableRequest: ISerializableUpdateRequest = Object.create(null); + const serializableRequest: ISerializableUpdateRequest = { workspace: this.workspace }; if (request.insert) { serializableRequest.insert = Array.from(request.insert.entries()); @@ -200,15 +177,66 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS return this.channel.call('updateItems', serializableRequest); } + abstract close(recovery?: () => Map): Promise; +} + +class GlobalStorageDatabase extends BaseStorageDatabase implements IStorageDatabase { + + private readonly _onDidChangeItemsExternal = this._register(new Emitter()); + readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; + + private onDidChangeGlobalStorageListener: IDisposable | undefined; + + constructor(channel: IChannel) { + super(channel, undefined); + + this.registerListeners(); + } + + private registerListeners(): void { + this.onDidChangeGlobalStorageListener = this._register(this.channel.listen('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(e.deleted) : undefined + }); + } + } + async close(): Promise { - // 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); + // when we are about to close, we start to ignore global storage changes since we close anyway + dispose(this.onDidChangeGlobalStorageListener); } } + +class WorkspaceStorageDatabase extends BaseStorageDatabase implements IStorageDatabase { + + readonly onDidChangeItemsExternal = Event.None; // unsupported for workspace storage because we only ever write from one window + + constructor(channel: IChannel, workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier) { + super(channel, workspace); + } + + async close(): Promise { + // TODO@bpasero close workspace storage? + } +} + +export class StorageDatabaseChannelClient extends Disposable { + + readonly globalStorage = new GlobalStorageDatabase(this.channel); + readonly workspaceStorage = this.workspace ? new WorkspaceStorageDatabase(this.channel, this.workspace) : undefined; + + constructor( + private channel: IChannel, + private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined + ) { + super(); + } +} + +//#endregion diff --git a/src/vs/platform/storage/node/storageMain.ts b/src/vs/platform/storage/node/storageMain.ts new file mode 100644 index 00000000000..4fd2e233149 --- /dev/null +++ b/src/vs/platform/storage/node/storageMain.ts @@ -0,0 +1,370 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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'; +import { currentSessionDateStorageKey, firstSessionDateStorageKey, instanceStorageKey, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { generateUuid } from 'vs/base/common/uuid'; + +/** + * Provides access to global and workspace storage from the + * electron-main side that is the owner of all storage connections. + */ +export interface IStorageMain { + + /** + * Emitted whenever data is updated or deleted. + */ + readonly onDidChangeStorage: Event; + + /** + * 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; + + /** + * Access to all cached items of this storage service. + */ + readonly items: Map; + + /** + * Required call to ensure the service can be used. + */ + initialize(): Promise; + + /** + * 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; + + /** + * Close the storage connection. + */ + close(): Promise; +} + +export interface IStorageChangeEvent { + key: string; +} + +export class GlobalStorageMain extends Disposable implements IStorageMain { + + private static readonly STORAGE_NAME = 'state.vscdb'; + + private readonly _onDidChangeStorage = this._register(new Emitter()); + readonly onDidChangeStorage = this._onDidChangeStorage.event; + + private readonly _onWillSaveState = this._register(new Emitter()); + readonly onWillSaveState = this._onWillSaveState.event; + + get items(): Map { return this.storage.items; } + + private storage: IStorage; + + private initializePromise: Promise | 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, GlobalStorageMain.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 { + if (!this.initializePromise) { + this.initializePromise = this.doInitialize(); + } + + return this.initializePromise; + } + + private async doInitialize(): Promise { + 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); + } + + // Apply global telemetry values as part of the initialization + this.storeTelemetryStateOnce(); + } + + private storeTelemetryStateOnce(): void { + const instanceId = this.get(instanceStorageKey, undefined); + if (instanceId === undefined) { + this.store(instanceStorageKey, generateUuid()); + } + + const firstSessionDate = this.get(firstSessionDateStorageKey, undefined); + if (firstSessionDate === undefined) { + this.store(firstSessionDateStorageKey, new Date().toUTCString()); + } + + const lastSessionDate = this.get(currentSessionDateStorageKey, undefined); // previous session date was the "current" one at that time + const currentSessionDate = new Date().toUTCString(); // current session date is "now" + this.store(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate); + this.store(currentSessionDateStorageKey, currentSessionDate); + } + + 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 { + return this.storage.set(key, value); + } + + remove(key: string): Promise { + return this.storage.delete(key); + } + + close(): Promise { + + // Signal as event so that clients can still store data + this._onWillSaveState.fire(); + + // Do it + return this.storage.close(); + } +} + +export class WorkspaceStorageMain extends Disposable implements IStorageMain { + + readonly onDidChangeStorage = Event.None; + readonly onWillSaveState = Event.None; + + get items(): Map { return this.storage.items; } + + private storage: IStorage; + + private initializePromise: Promise | undefined; + + constructor( + ) { + super(); + + // Until the storage has been initialized, it can only be in memory + this.storage = new Storage(new InMemoryStorageDatabase()); + } + + async initialize(): Promise { + if (!this.initializePromise) { + this.initializePromise = this.doInitialize(); + } + + return this.initializePromise; + } + + private async doInitialize(): Promise { + // private async initializeWorkspaceStorage(payload: IWorkspaceInitializationPayload): Promise { + + // // 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('code/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('code/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.emitDidChangeValue(StorageScope.WORKSPACE, key)); + + // 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 promises.mkdir(workspaceStorageFolderPath, { recursive: true }); + + // // Write metadata into folder + // this.ensureWorkspaceStorageFolderMeta(payload); + + // return { path: workspaceStorageFolderPath, wasCreated: true }; + // } + + // private ensureWorkspaceStorageFolderMeta(payload: IWorkspaceInitializationPayload): void { + // let meta: object | undefined = undefined; + // if (isSingleFolderWorkspaceIdentifier(payload)) { + // meta = { folder: payload.uri.toString() }; + // } else if (isWorkspaceIdentifier(payload)) { + // meta = { workspace: payload.configPath.toString() }; + // } + + // if (meta) { + // (async () => { + // try { + // const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME); + // const storageExists = await exists(workspaceStorageMetaPath); + // if (!storageExists) { + // await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); + // } + // } catch (error) { + // this.logService.error(error); + // } + // })(); + // } + // } + } + + 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 { + return this.storage.set(key, value); + } + + remove(key: string): Promise { + return this.storage.delete(key); + } + + close(): Promise { + return this.storage.close(); + } +} diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts index 0d25d4e6554..0e4920b6fd0 100644 --- a/src/vs/platform/storage/node/storageMainService.ts +++ b/src/vs/platform/storage/node/storageMainService.ts @@ -3,15 +3,11 @@ * 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'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { GlobalStorageMain, IStorageMain, WorkspaceStorageMain } from 'vs/platform/storage/node/storageMain'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export const IStorageMainService = createDecorator('storageMainService'); @@ -20,172 +16,50 @@ export interface IStorageMainService { readonly _serviceBrand: undefined; /** - * Emitted whenever data is updated or deleted. + * Provides access to the global storage shared across all windows. */ - readonly onDidChangeStorage: Event; + readonly globalStorage: IStorageMain; /** - * 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. + * Provides access to the workspace storage specific to a single window. */ - readonly onWillSaveState: Event; - - /** - * Access to all cached items of this storage service. - */ - readonly items: Map; - - /** - * Required call to ensure the service can be used. - */ - initialize(): Promise; - - /** - * 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; + workspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): IStorageMain; } -export interface IStorageChangeEvent { - key: string; -} - -export class StorageMainService extends Disposable implements IStorageMainService { +export class StorageMainService implements IStorageMainService { declare readonly _serviceBrand: undefined; - private static readonly STORAGE_NAME = 'state.vscdb'; + readonly globalStorage = this.createGlobalStorage(); - private readonly _onDidChangeStorage = this._register(new Emitter()); - readonly onDidChangeStorage = this._onDidChangeStorage.event; - - private readonly _onWillSaveState = this._register(new Emitter()); - readonly onWillSaveState = this._onWillSaveState.event; - - get items(): Map { return this.storage.items; } - - private storage: IStorage; - - private initializePromise: Promise | undefined; + private readonly mapWorkspaceToStorage = new Map(); 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! + private createGlobalStorage(): IStorageMain { + const globalStorage = new GlobalStorageMain(this.logService, this.environmentService); + + return globalStorage; + } + + private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): IStorageMain { + const workspaceStorage = new WorkspaceStorageMain(); + // TODO@bpasero lifecycle like global storage? window events? crashes? + + return workspaceStorage; + } + + workspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): IStorageMain { + let workspaceStorage = this.mapWorkspaceToStorage.get(workspace.id); + if (!workspaceStorage) { + workspaceStorage = this.createWorkspaceStorage(workspace); + this.mapWorkspaceToStorage.set(workspace.id, workspaceStorage); } - 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 { - if (!this.initializePromise) { - this.initializePromise = this.doInitialize(); - } - - return this.initializePromise; - } - - private async doInitialize(): Promise { - 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 { - return this.storage.set(key, value); - } - - remove(key: string): Promise { - return this.storage.delete(key); - } - - close(): Promise { - - // Signal as event so that clients can still store data - this._onWillSaveState.fire(); - - // Do it - return this.storage.close(); + return workspaceStorage; } } diff --git a/src/vs/platform/storage/node/storageService2.ts b/src/vs/platform/storage/node/storageService2.ts new file mode 100644 index 00000000000..e5748a183f0 --- /dev/null +++ b/src/vs/platform/storage/node/storageService2.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { StorageScope, WillSaveStateReason, logStorage, AbstractStorageService } from 'vs/platform/storage/common/storage'; +import { Storage, IStorageDatabase, IStorage } from 'vs/base/parts/storage/common/storage'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; +import { assertIsDefined } from 'vs/base/common/types'; +import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; + +export class NativeStorageService2 extends AbstractStorageService { + + private readonly globalStorage = new Storage(this.globalStorageDatabase); + private readonly workspaceStorage = this.workspaceStorageDatabase ? new Storage(this.workspaceStorageDatabase) : undefined; + + private initializePromise: Promise | undefined; + + private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 60000 /* every minute */)); + private runWhenIdleDisposable: IDisposable | undefined = undefined; + + constructor( + private globalStorageDatabase: IStorageDatabase, + private workspaceStorageDatabase: IStorageDatabase | undefined, + @IEnvironmentService private readonly environmentService: IEnvironmentService + ) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key))); + this._register(this.workspaceStorage?.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)) ?? Disposable.None); + } + + initialize(): Promise { + if (!this.initializePromise) { + this.initializePromise = this.doInitialize(); + } + + return this.initializePromise; + } + + private async doInitialize(): Promise { + + // Init all storage locations + await Promises.settled([ + this.globalStorage.init(), + this.workspaceStorage?.init() ?? 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(); + } + + 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); + } + + protected doStore(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void { + this.getStorage(scope).set(key, value); + } + + 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); + } + + protected async doFlush(): Promise { + await Promises.settled([ + this.globalStorage.whenFlushed(), + this.workspaceStorage?.whenFlushed() ?? Promise.resolve() + ]); + } + + 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(); + }); + } + + async close(): Promise { + + // 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.emitWillSaveState(WillSaveStateReason.SHUTDOWN); + + // Do it + await Promises.settled([ + this.globalStorage.close(), + this.workspaceStorage?.close() ?? Promise.resolve() + ]); + } + + async logStorage(): Promise { + return logStorage( + this.globalStorage.items, + this.workspaceStorage ? this.workspaceStorage.items : new Map(), + this.environmentService.globalStorageHome.fsPath, + /* this.workspaceStoragePath || */ ''); + } + + async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise { + // 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, { preserveSymlinks: false }); + + // // Recreate and init workspace storage + // return this.createWorkspaceStorage(newWorkspaceStoragePath).init(); + } +} diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 2ae70d36bce..3ca7fa2b745 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -114,6 +114,7 @@ export interface IWindowSettings { readonly enableMenuBarMnemonics: boolean; readonly closeWhenEmpty: boolean; readonly clickThroughInactive: boolean; + readonly enableExperimentalMainProcessWorkspaceStorage: boolean; } export function getTitleBarStyle(configurationService: IConfigurationService): 'native' | 'custom' { @@ -253,6 +254,8 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native filesToWait?: IPathsToWaitFor; os: IOSConfiguration; + + enableExperimentalMainProcessWorkspaceStorage: boolean; } /** diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index 7fc86f531bd..2d60e42471c 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -114,9 +114,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { constructor( config: IWindowCreationOptions, @ILogService private readonly logService: ILogService, - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IFileService private readonly fileService: IFileService, - @IStorageMainService storageService: IStorageMainService, + @IStorageMainService storageMainService: IStorageMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @@ -158,7 +158,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { nativeWindowOpen: true, webviewTag: true, zoomFactor: zoomLevelToZoomFactor(windowConfig?.zoomLevel), - ...this.environmentService.sandbox ? + ...this.environmentMainService.sandbox ? // Sandbox { @@ -181,9 +181,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Linux: always // Windows: only when running out of sources, otherwise an icon is set by us on the executable if (isLinux) { - options.icon = join(this.environmentService.appRoot, 'resources/linux/code.png'); - } else if (isWindows && !this.environmentService.isBuilt) { - options.icon = join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); + options.icon = join(this.environmentMainService.appRoot, 'resources/linux/code.png'); + } else if (isWindows && !this.environmentMainService.isBuilt) { + options.icon = join(this.environmentMainService.appRoot, 'resources/win32/code_150x150.png'); } if (isMacintosh && !this.useNativeFullScreen()) { @@ -217,7 +217,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._id = this._win.id; // Open devtools if instructed from command line args - if (this.environmentService.args['open-devtools'] === true) { + if (this.environmentMainService.args['open-devtools'] === true) { this._win.webContents.openDevTools(); } @@ -267,9 +267,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.createTouchBar(); // Request handling - this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentService, this.fileService, { - get(key) { return storageService.get(key); }, - store(key, value) { storageService.store(key, value); } + this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentMainService, this.fileService, { + get: key => storageMainService.globalStorage.get(key), + store: (key, value) => storageMainService.globalStorage.store(key, value) }); // Eventing @@ -535,20 +535,23 @@ export class CodeWindow extends Disposable implements ICodeWindow { }); // Handle configuration changes - this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated())); + this._register(this.configurationService.onDidChangeConfiguration(() => this.onConfigurationUpdated())); // Handle Workspace events this._register(this.workspacesManagementMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e))); // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; - this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => - this.marketplaceHeadersPromise.then(headers => cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) }))); + this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => { + const headers = await this.marketplaceHeadersPromise; + + cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) }); + }); } - private onWindowError(error: WindowError.UNRESPONSIVE): void; - private onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): void; - private onWindowError(error: WindowError, details?: RenderProcessGoneDetails): void { + private async onWindowError(error: WindowError.UNRESPONSIVE): Promise; + private async onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): Promise; + private async onWindowError(error: WindowError, details?: RenderProcessGoneDetails): Promise { this.logService.error(error === WindowError.CRASHED ? `Main: renderer process crashed (detail: ${details?.reason})` : 'Main: detected unresponsive'); // If we run extension tests from CLI, showing a dialog is not @@ -581,25 +584,25 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Show Dialog - this.dialogMainService.showMessageBox({ + const result = await this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], message: localize('appStalled', "The window is no longer responding"), detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."), noLink: true - }, this._win).then(result => { - if (!this._win) { - return; // Return early if the window has been going down already - } + }, this._win); - if (result.response === 0) { - this._win.webContents.forcefullyCrashRenderer(); // Calling reload() immediately after calling this method will force the reload to occur in a new process - this.reload(); - } else if (result.response === 2) { - this.destroyWindow(); - } - }); + if (!this._win) { + return; // Return early if the window has been going down already + } + + if (result.response === 0) { + this._win.webContents.forcefullyCrashRenderer(); // Calling reload() immediately after calling this method will force the reload to occur in a new process + this.reload(); + } else if (result.response === 2) { + this.destroyWindow(); + } } // Crashed @@ -611,24 +614,24 @@ export class CodeWindow extends Disposable implements ICodeWindow { message = localize('appCrashed', "The window has crashed", details?.reason); } - this.dialogMainService.showMessageBox({ + const result = await this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], message, detail: localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), noLink: true - }, this._win).then(result => { - if (!this._win) { - return; // Return early if the window has been going down already - } + }, this._win); - if (result.response === 0) { - this.reload(); - } else if (result.response === 1) { - this.destroyWindow(); - } - }); + if (!this._win) { + return; // Return early if the window has been going down already + } + + if (result.response === 0) { + this.reload(); + } else if (result.response === 1) { + this.destroyWindow(); + } } } @@ -747,7 +750,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Make window visible if it did not open in N seconds because this indicates an error // Only do this when running out of sources and not when running tests - if (!this.environmentService.isBuilt && !this.environmentService.extensionTestsLocationURI) { + if (!this.environmentMainService.isBuilt && !this.environmentMainService.extensionTestsLocationURI) { this.showTimeoutHandle = setTimeout(() => { if (this._win && !this._win.isVisible() && !this._win.isMinimized()) { this._win.show(); @@ -824,7 +827,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.windowId = this._win.id; windowConfiguration.sessionId = `window:${this._win.id}`; windowConfiguration.logLevel = this.logService.getLevel(); - windowConfiguration.logsPath = this.environmentService.logsPath; + windowConfiguration.logsPath = this.environmentMainService.logsPath; // Set zoomlevel const windowConfig = this.configurationService.getValue('window'); @@ -851,13 +854,15 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.perfMarks = getMarks(); // Parts splash - windowConfiguration.partsSplashPath = join(this.environmentService.userDataPath, 'rapid_render.json'); + windowConfiguration.partsSplashPath = join(this.environmentMainService.userDataPath, 'rapid_render.json'); // OS Info windowConfiguration.os = { release: release() }; + windowConfiguration.enableExperimentalMainProcessWorkspaceStorage = !!(windowConfig?.enableExperimentalMainProcessWorkspaceStorage); + // Config (combination of process.argv and window configuration) const environment = parseArgs(process.argv, OPTIONS); const config = Object.assign(environment, windowConfiguration) as unknown as { [key: string]: unknown }; @@ -887,7 +892,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private doGetUrl(config: object): string { let workbench: string; - if (this.environmentService.sandbox) { + if (this.environmentMainService.sandbox) { workbench = 'vs/code/electron-sandbox/workbench/workbench.html'; } else { workbench = 'vs/code/electron-browser/workbench/workbench.html'; diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index a6b22ab4def..24a5f7361c7 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -18,11 +18,12 @@ import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { INativeWorkbenchConfiguration, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IWorkspaceInitializationPayload, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { NativeStorageService } from 'vs/platform/storage/node/storageService'; +import { NativeStorageService2 } from 'vs/platform/storage/node/storageService2'; import { Schemas } from 'vs/base/common/network'; -import { GlobalStorageDatabaseChannelClient } from 'vs/platform/storage/node/storageIpc'; +import { StorageDatabaseChannelClient } from 'vs/platform/storage/node/storageIpc'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -130,14 +131,14 @@ class DesktopMain extends Disposable { services.logService.trace('workbench configuration', JSON.stringify(this.configuration)); } - private registerListeners(workbench: Workbench, storageService: NativeStorageService): void { + private registerListeners(workbench: Workbench, storageService: NativeStorageService | NativeStorageService2): void { // Workbench Lifecycle this._register(workbench.onShutdown(() => this.dispose())); this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage'))); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: NativeStorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: NativeStorageService | NativeStorageService2 }> { const serviceCollection = new ServiceCollection(); @@ -319,9 +320,15 @@ class DesktopMain extends Disposable { } } - private async createStorageService(payload: IWorkspaceInitializationPayload, logService: ILogService, mainProcessService: IMainProcessService): Promise { - const globalStorageDatabase = new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')); - const storageService = new NativeStorageService(globalStorageDatabase, logService, this.environmentService); + private async createStorageService(payload: IWorkspaceInitializationPayload, logService: ILogService, mainProcessService: IMainProcessService): Promise { + const storageDataBase = new StorageDatabaseChannelClient(mainProcessService.getChannel('storage'), isWorkspaceIdentifier(payload) || isSingleFolderWorkspaceIdentifier(payload) ? payload : undefined); + + let storageService: NativeStorageService | NativeStorageService2; + if (this.configuration.enableExperimentalMainProcessWorkspaceStorage) { + storageService = new NativeStorageService2(storageDataBase.globalStorage, storageDataBase.workspaceStorage, this.environmentService); + } else { + storageService = new NativeStorageService(storageDataBase.globalStorage, logService, this.environmentService); + } try { await storageService.initialize(payload); diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 299561eb5e1..1b1971fd14e 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -273,6 +273,12 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; 'scope': ConfigurationScope.APPLICATION, 'description': localize('window.clickThroughInactive', "If enabled, clicking on an inactive window will both activate the window and trigger the element under the mouse if it is clickable. If disabled, clicking anywhere on an inactive window will activate it only and a second click is required on the element."), 'included': isMacintosh + }, + 'window.enableExperimentalMainProcessWorkspaceStorage': { + 'type': 'boolean', + 'default': false, + 'scope': ConfigurationScope.APPLICATION, + 'description': localize('window.localize', "Enables workspace storage access from the main process. Requires a restart to take effect."), } } }); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index f2c26043db4..6dbb50fa818 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -55,6 +55,7 @@ export const TestWorkbenchConfiguration: INativeWorkbenchConfiguration = { perfMarks: [], colorScheme: { dark: true, highContrast: false }, os: { release: release() }, + enableExperimentalMainProcessWorkspaceStorage: false, ...parseArgs(process.argv, OPTIONS) };