diff --git a/dev-packages/application-manager/src/generator/backend-generator.ts b/dev-packages/application-manager/src/generator/backend-generator.ts index f7de3c37b49f4..99f11e8f68c54 100644 --- a/dev-packages/application-manager/src/generator/backend-generator.ts +++ b/dev-packages/application-manager/src/generator/backend-generator.ts @@ -91,7 +91,7 @@ const serverModule = require('./server'); const serverAddress = main.start(serverModule()); serverAddress.then(({ port, address }) => { - if (process && process.send) { + if (process.send) { process.send({ port, address }); } }); diff --git a/dev-packages/localization-manager/src/localization-manager.ts b/dev-packages/localization-manager/src/localization-manager.ts index 04e3187fba7c5..67fe17d004300 100644 --- a/dev-packages/localization-manager/src/localization-manager.ts +++ b/dev-packages/localization-manager/src/localization-manager.ts @@ -17,8 +17,7 @@ import * as chalk from 'chalk'; import * as fs from 'fs-extra'; import * as path from 'path'; -import { sortLocalization } from '.'; -import { Localization } from './common'; +import { Localization, sortLocalization } from './common'; import { deepl, DeeplLanguage, DeeplParameters, isSupportedLanguage, supportedLanguages } from './deepl-api'; export interface LocalizationOptions { diff --git a/dev-packages/private-re-exports/src/package-re-exports.ts b/dev-packages/private-re-exports/src/package-re-exports.ts index 6d37d6ce7c748..2c1e0fdb67ed1 100644 --- a/dev-packages/private-re-exports/src/package-re-exports.ts +++ b/dev-packages/private-re-exports/src/package-re-exports.ts @@ -17,8 +17,7 @@ import cp = require('child_process'); import fs = require('fs'); import path = require('path'); -import { parseModule } from '.'; -import { PackageJson, ReExportJson } from './utility'; +import { PackageJson, parseModule, ReExportJson } from './utility'; export async function readJson(jsonPath: string): Promise { return JSON.parse(await fs.promises.readFile(jsonPath, 'utf8')) as T; diff --git a/doc/Migration.md b/doc/Migration.md index a7493f02aa72c..6d0ad65db2cbe 100644 --- a/doc/Migration.md +++ b/doc/Migration.md @@ -42,14 +42,14 @@ This version updates the Monaco code used in Theia to the state of VSCode 1.65.2 #### ASM to ESM -Two kinds of changes may be required to consume Monaco using ESM modules. +Two kinds of changes may be required to consume Monaco using ESM modules. - If your application uses its own Webpack config rather than that generated by the @theia/dev-packages, you will need to update that config to remove the `CopyWebpackPlugin` formerly used to place Monaco code in the build folder and to build a separate entrypoint for the `editor.worker`. See [the changes here](https://github.com/eclipse-theia/theia/pull/10736/files#diff-b4677f3ff57d8b952eeefc10493ed3600d2737f9b5c9b0630b172472acb9c3a2) - If your application uses its own frontend generator, you should modify the code that generates the `index.html` to load the `script` containing the bundle into the `body` element rather than the head. See [changes here](https://github.com/eclipse-theia/theia/pull/10947/files) - - References to the `window.monaco` object should be replaced with imports from `@theia/monaco-editor-core`. In most cases, simply adding an import `import * as monaco from -'@theia/monaco-editor-core'` will suffice. More complex use cases may require imports from specific parts of Monaco. Please see + - References to the `window.monaco` object should be replaced with imports from `@theia/monaco-editor-core`. In most cases, simply adding an import `import * as monaco from +'@theia/monaco-editor-core'` will suffice. More complex use cases may require imports from specific parts of Monaco. Please see [the PR](https://github.com/eclipse-theia/theia/pull/10736) for details, and please post any questions or problems there. Using ESM modules, it is now possible to follow imports to definitions and to the Monaco source code. This should aid in tracking down issues related to changes in Monaco discussed diff --git a/examples/api-samples/package.json b/examples/api-samples/package.json index b8a0401a7f4a7..45f822213d532 100644 --- a/examples/api-samples/package.json +++ b/examples/api-samples/package.json @@ -26,6 +26,10 @@ { "electronMain": "lib/electron-main/update/sample-updater-main-module", "frontendElectron": "lib/electron-browser/updater/sample-updater-frontend-module" + }, + { + "electronMain": "lib/electron-main/ipc/electron-main-ipc-module", + "backendElectron": "lib/electron-node/ipc/electron-backend-ipc-module" } ], "keywords": [ diff --git a/examples/api-samples/src/common/updater/sample-updater.ts b/examples/api-samples/src/common/updater/sample-updater.ts index 4860a3ea481da..f2cd744c217d5 100644 --- a/examples/api-samples/src/common/updater/sample-updater.ts +++ b/examples/api-samples/src/common/updater/sample-updater.ts @@ -13,7 +13,8 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; + +import { Event, serviceIdentifier, servicePath } from '@theia/core/lib/common'; export enum UpdateStatus { InProgress = 'in-progress', @@ -21,17 +22,11 @@ export enum UpdateStatus { NotAvailable = 'not-available' } -export const SampleUpdaterPath = '/services/sample-updater'; -export const SampleUpdater = Symbol('SampleUpdater'); -export interface SampleUpdater extends JsonRpcServer { +export const SampleUpdaterPath = servicePath('/services/sample-updater'); +export const SampleUpdater = serviceIdentifier('SampleUpdater'); +export interface SampleUpdater { + onReadyToInstall: Event; checkForUpdates(): Promise<{ status: UpdateStatus }>; onRestartToUpdateRequested(): void; - disconnectClient(client: SampleUpdaterClient): void; - setUpdateAvailable(available: boolean): Promise; // Mock } - -export const SampleUpdaterClient = Symbol('SampleUpdaterClient'); -export interface SampleUpdaterClient { - notifyReadyToInstall(): void; -} diff --git a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts index 5127e31375e89..e3845cd36cf84 100644 --- a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts +++ b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts @@ -20,7 +20,6 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify' import { isOSX } from '@theia/core/lib/common/os'; import { CommonMenus } from '@theia/core/lib/browser'; import { - Emitter, Command, MenuPath, MessageService, @@ -31,7 +30,7 @@ import { } from '@theia/core/lib/common'; import { ElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; -import { SampleUpdater, UpdateStatus, SampleUpdaterClient } from '../../common/updater/sample-updater'; +import { SampleUpdater, UpdateStatus } from '../../common/updater/sample-updater'; export namespace SampleUpdaterCommands { @@ -68,18 +67,6 @@ export namespace SampleUpdaterMenu { export const MENU_PATH: MenuPath = [...CommonMenus.FILE_SETTINGS_SUBMENU, '3_settings_submenu_update']; } -@injectable() -export class SampleUpdaterClientImpl implements SampleUpdaterClient { - - protected readonly onReadyToInstallEmitter = new Emitter(); - readonly onReadyToInstall = this.onReadyToInstallEmitter.event; - - notifyReadyToInstall(): void { - this.onReadyToInstallEmitter.fire(); - } - -} - // Dynamic menus aren't yet supported by electron: https://github.com/eclipse-theia/theia/issues/446 @injectable() export class ElectronMenuUpdater { @@ -113,14 +100,11 @@ export class SampleUpdaterFrontendContribution implements CommandContribution, M @inject(SampleUpdater) protected readonly updater: SampleUpdater; - @inject(SampleUpdaterClientImpl) - protected readonly updaterClient: SampleUpdaterClientImpl; - protected readyToUpdate = false; @postConstruct() protected init(): void { - this.updaterClient.onReadyToInstall(async () => { + this.updater.onReadyToInstall(async () => { this.readyToUpdate = true; this.menuUpdater.update(); this.handleUpdatesAvailable(); @@ -138,7 +122,7 @@ export class SampleUpdaterFrontendContribution implements CommandContribution, M } case UpdateStatus.NotAvailable: { const { applicationName } = FrontendApplicationConfigProvider.get(); - this.messageService.info(`[Not Available]: You’re all good. You’ve got the latest version of ${applicationName}.`, { timeout: 3000 }); + this.messageService.info(`[Not Available]: You're all good. You've got the latest version of ${applicationName}.`, { timeout: 3000 }); break; } case UpdateStatus.InProgress: { diff --git a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts index bc7ab932e3114..bcf3e3ddef798 100644 --- a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts +++ b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts @@ -14,21 +14,17 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +import { bindContribution, CommandContribution, MenuContribution, ProxyProvider } from '@theia/core/lib/common'; +import { ElectronMainAndFrontend } from '@theia/core/lib/electron-common'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; -import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; -import { SampleUpdater, SampleUpdaterPath, SampleUpdaterClient } from '../../common/updater/sample-updater'; -import { SampleUpdaterFrontendContribution, ElectronMenuUpdater, SampleUpdaterClientImpl } from './sample-updater-frontend-contribution'; +import { SampleUpdater, SampleUpdaterPath } from '../../common/updater/sample-updater'; +import { ElectronMenuUpdater, SampleUpdaterFrontendContribution } from './sample-updater-frontend-contribution'; export default new ContainerModule(bind => { + bind(SampleUpdater) + .toDynamicValue(ctx => ctx.container.getNamed(ProxyProvider, ElectronMainAndFrontend).getProxy(SampleUpdaterPath)) + .inSingletonScope(); bind(ElectronMenuUpdater).toSelf().inSingletonScope(); - bind(SampleUpdaterClientImpl).toSelf().inSingletonScope(); - bind(SampleUpdaterClient).toService(SampleUpdaterClientImpl); - bind(SampleUpdater).toDynamicValue(context => { - const client = context.container.get(SampleUpdaterClientImpl); - return ElectronIpcConnectionProvider.createProxy(context.container, SampleUpdaterPath, client); - }).inSingletonScope(); bind(SampleUpdaterFrontendContribution).toSelf().inSingletonScope(); - bind(MenuContribution).toService(SampleUpdaterFrontendContribution); - bind(CommandContribution).toService(SampleUpdaterFrontendContribution); + bindContribution(bind, SampleUpdaterFrontendContribution, [MenuContribution, CommandContribution]); }); diff --git a/examples/api-samples/src/electron-common/ipc/electron-ipc.ts b/examples/api-samples/src/electron-common/ipc/electron-ipc.ts new file mode 100644 index 0000000000000..c9938972e0264 --- /dev/null +++ b/examples/api-samples/src/electron-common/ipc/electron-ipc.ts @@ -0,0 +1,23 @@ +// ***************************************************************************** +// Copyright (C) 2022 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { serviceIdentifier, servicePath } from '@theia/core/lib/common'; + +export const ELECTRON_MAIN_AND_BACKEND_IPC_SAMPLE_PATH = servicePath('/services/test-connection'); +export const ElectronMainAndBackendIpcSample = serviceIdentifier('TestConnection'); +export interface ElectronMainAndBackendIpcSample { + getBrowserWindowTitles(): Promise +} diff --git a/packages/core/src/electron-common/messaging/electron-connection-handler.ts b/examples/api-samples/src/electron-main/ipc/electron-main-ipc-impl.ts similarity index 59% rename from packages/core/src/electron-common/messaging/electron-connection-handler.ts rename to examples/api-samples/src/electron-main/ipc/electron-main-ipc-impl.ts index a7fe3e9c92b81..8d456f253eda6 100644 --- a/packages/core/src/electron-common/messaging/electron-connection-handler.ts +++ b/examples/api-samples/src/electron-main/ipc/electron-main-ipc-impl.ts @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright (C) 2020 Ericsson and others. +// Copyright (C) 2022 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at @@ -14,17 +14,14 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { ConnectionHandler } from '../../common/messaging/handler'; +import { BrowserWindow } from '@theia/core/electron-shared/electron'; +import { injectable } from '@theia/core/shared/inversify'; +import { ElectronMainAndBackendIpcSample } from '../../electron-common/ipc/electron-ipc'; -/** - * Name of the channel used with `ipcMain.on/emit`. - */ -export const THEIA_ELECTRON_IPC_CHANNEL_NAME = 'theia-electron-ipc'; +@injectable() +export class ElectronMainAndBackendIpcSampleImpl implements ElectronMainAndBackendIpcSample { -/** - * Electron-IPC-specific connection handler. - * Use this if you want to establish communication between the frontend and the electron-main process. - */ -export const ElectronConnectionHandler = Symbol('ElectronConnectionHandler'); -export interface ElectronConnectionHandler extends ConnectionHandler { + async getBrowserWindowTitles(): Promise { + return BrowserWindow.getAllWindows().map(browserWindow => browserWindow.getTitle()); + } } diff --git a/examples/api-samples/src/electron-main/ipc/electron-main-ipc-module.ts b/examples/api-samples/src/electron-main/ipc/electron-main-ipc-module.ts new file mode 100644 index 0000000000000..68b8ff90e099f --- /dev/null +++ b/examples/api-samples/src/electron-main/ipc/electron-main-ipc-module.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson// ***************************************************************************** +// Copyright (C) 2020 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ServiceContribution } from '@theia/core/lib/common'; +import { ElectronMainAndBackend } from '@theia/core/lib/electron-common'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { ElectronMainAndBackendIpcSample, ELECTRON_MAIN_AND_BACKEND_IPC_SAMPLE_PATH } from '../../electron-common/ipc/electron-ipc'; +import { ElectronMainAndBackendIpcSampleImpl } from './electron-main-ipc-impl'; + +export default new ContainerModule(bind => { + bind(ElectronMainAndBackendIpcSample).to(ElectronMainAndBackendIpcSampleImpl).inSingletonScope(); + bind(ServiceContribution) + .toDynamicValue(ctx => ({ + [ELECTRON_MAIN_AND_BACKEND_IPC_SAMPLE_PATH]: () => ctx.container.get(ElectronMainAndBackendIpcSample) + })) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndBackend); +}); diff --git a/examples/api-samples/src/electron-main/update/sample-updater-impl.ts b/examples/api-samples/src/electron-main/update/sample-updater-impl.ts index 59467a5cb9a12..bb6ee3ab5dad5 100644 --- a/examples/api-samples/src/electron-main/update/sample-updater-impl.ts +++ b/examples/api-samples/src/electron-main/update/sample-updater-impl.ts @@ -16,15 +16,20 @@ import { injectable } from '@theia/core/shared/inversify'; import { ElectronMainApplication, ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; -import { SampleUpdater, SampleUpdaterClient, UpdateStatus } from '../../common/updater/sample-updater'; +import { SampleUpdater, UpdateStatus } from '../../common/updater/sample-updater'; +import { Emitter, Event } from '@theia/core/lib/common'; @injectable() export class SampleUpdaterImpl implements SampleUpdater, ElectronMainApplicationContribution { - protected clients: Array = []; + protected onReadyToInstallEmitter = new Emitter(); protected inProgressTimer: NodeJS.Timer | undefined; protected available = false; + get onReadyToInstall(): Event { + return this.onReadyToInstallEmitter.event; + } + async checkForUpdates(): Promise<{ status: UpdateStatus }> { if (this.inProgressTimer) { return { status: UpdateStatus.InProgress }; @@ -48,9 +53,7 @@ export class SampleUpdaterImpl implements SampleUpdater, ElectronMainApplication this.inProgressTimer = setTimeout(() => { this.inProgressTimer = undefined; this.available = true; - for (const client of this.clients) { - client.notifyReadyToInstall(); - } + this.onReadyToInstallEmitter.fire(); }, 5000); } } @@ -62,30 +65,4 @@ export class SampleUpdaterImpl implements SampleUpdater, ElectronMainApplication onStop(application: ElectronMainApplication): void { // Invoked when the contribution is stopping. You can clean up things here. You are not allowed call async code from here. } - - setClient(client: SampleUpdaterClient | undefined): void { - if (client) { - this.clients.push(client); - console.info('Registered a new sample updater client.'); - } else { - console.warn("Couldn't register undefined client."); - } - } - - disconnectClient(client: SampleUpdaterClient): void { - const index = this.clients.indexOf(client); - if (index !== -1) { - this.clients.splice(index, 1); - console.info('Disposed a sample updater client.'); - } else { - console.warn("Couldn't dispose client; it was not registered."); - } - } - - dispose(): void { - console.info('>>> Disposing sample updater service...'); - this.clients.forEach(this.disconnectClient.bind(this)); - console.info('>>> Disposed sample updater service.'); - } - } diff --git a/examples/api-samples/src/electron-main/update/sample-updater-main-module.ts b/examples/api-samples/src/electron-main/update/sample-updater-main-module.ts index 8e099425be8fc..17cc298a27185 100644 --- a/examples/api-samples/src/electron-main/update/sample-updater-main-module.ts +++ b/examples/api-samples/src/electron-main/update/sample-updater-main-module.ts @@ -15,22 +15,27 @@ // ***************************************************************************** import { ContainerModule } from '@theia/core/shared/inversify'; -import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory'; import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; -import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler'; -import { SampleUpdaterPath, SampleUpdater, SampleUpdaterClient } from '../../common/updater/sample-updater'; +import { SampleUpdaterPath, SampleUpdater } from '../../common/updater/sample-updater'; import { SampleUpdaterImpl } from './sample-updater-impl'; +import { ServiceContribution } from '@theia/core/lib/common'; +import { ElectronMainAndFrontend } from '@theia/core/lib/electron-common'; + +export const SampleUpdaterElectronMainAndFrontendContainerModule = new ContainerModule(bind => { + bind(ServiceContribution) + .toDynamicValue(ctx => ({ + // This will return the same singleton instance from the main container module + // for `SampleUpdater`, but this is by design here. + [SampleUpdaterPath]: () => ctx.container.get(SampleUpdater) + })) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndFrontend); +}); export default new ContainerModule(bind => { - bind(SampleUpdaterImpl).toSelf().inSingletonScope(); - bind(SampleUpdater).toService(SampleUpdaterImpl); + bind(SampleUpdater).to(SampleUpdaterImpl).inSingletonScope(); bind(ElectronMainApplicationContribution).toService(SampleUpdater); - bind(ElectronConnectionHandler).toDynamicValue(context => - new JsonRpcConnectionHandler(SampleUpdaterPath, client => { - const server = context.container.get(SampleUpdater); - server.setClient(client); - client.onDidCloseConnection(() => server.disconnectClient(client)); - return server; - }) - ).inSingletonScope(); + bind(ContainerModule) + .toConstantValue(SampleUpdaterElectronMainAndFrontendContainerModule) + .whenTargetNamed(ElectronMainAndFrontend); }); diff --git a/examples/api-samples/src/electron-node/ipc/electron-backend-ipc-contribution.ts b/examples/api-samples/src/electron-node/ipc/electron-backend-ipc-contribution.ts new file mode 100644 index 0000000000000..852c55b5ebf4d --- /dev/null +++ b/examples/api-samples/src/electron-node/ipc/electron-backend-ipc-contribution.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { BackendApplicationContribution } from '@theia/core/lib/node'; +import { ElectronMainAndBackendIpcSample } from '../../electron-common/ipc/electron-ipc'; + +@injectable() +export class ElectronMainAndBackendIpcSampleContribution implements BackendApplicationContribution { + + @inject(ElectronMainAndBackendIpcSample) + protected electronMainAndBackendIpcSample: ElectronMainAndBackendIpcSample; + + onStart(): void { + setInterval(async () => { + console.log('Window Titles:', await this.electronMainAndBackendIpcSample.getBrowserWindowTitles()); + }, 5000); + } +} diff --git a/examples/api-samples/src/electron-node/ipc/electron-backend-ipc-module.ts b/examples/api-samples/src/electron-node/ipc/electron-backend-ipc-module.ts new file mode 100644 index 0000000000000..3769586398364 --- /dev/null +++ b/examples/api-samples/src/electron-node/ipc/electron-backend-ipc-module.ts @@ -0,0 +1,31 @@ +// ***************************************************************************** +// Copyright (C) 2022 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ProxyProvider } from '@theia/core'; +import { ElectronMainAndBackend } from '@theia/core/lib/electron-common'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { BackendApplicationContribution } from '@theia/core/lib/node'; +import { ElectronMainAndBackendIpcSample, ELECTRON_MAIN_AND_BACKEND_IPC_SAMPLE_PATH } from '../../electron-common/ipc/electron-ipc'; +import { ElectronMainAndBackendIpcSampleContribution } from './electron-backend-ipc-contribution'; + +export default new ContainerModule(bind => { + bind(ElectronMainAndBackendIpcSample) + .toDynamicValue(ctx => ctx.container.getNamed(ProxyProvider, ElectronMainAndBackend).getProxy(ELECTRON_MAIN_AND_BACKEND_IPC_SAMPLE_PATH)) + .inSingletonScope(); + bind(BackendApplicationContribution) + .to(ElectronMainAndBackendIpcSampleContribution) + .inSingletonScope(); +}); diff --git a/license-check-baseline.json b/license-check-baseline.json index de73cf88ad1f5..09ab2cce1fe5d 100644 --- a/license-check-baseline.json +++ b/license-check-baseline.json @@ -1,4 +1,5 @@ { + "npm/npmjs/-/doctrine/2.1.0": "Approved: https://gitlab.eclipse.org/eclipsefdn/emo-team/iplab/-/issues/1987", "npm/npmjs/-/eslint-plugin-deprecation/1.2.1": "Approved as 'works-with': https://dev.eclipse.org/ipzilla/show_bug.cgi?id=22573", "npm/npmjs/-/jschardet/2.3.0": "Approved for Eclipse Theia: https://dev.eclipse.org/ipzilla/show_bug.cgi?id=22481", "npm/npmjs/-/jsdom/11.12.0": "Approved as 'works-with': https://dev.eclipse.org/ipzilla/show_bug.cgi?id=23640https://dev.eclipse.org/ipzilla/show_bug.cgi?id=23640", diff --git a/packages/core/package.json b/packages/core/package.json index 676fd598e927b..d8deeb07da784 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -66,6 +66,7 @@ "socket.io": "4.4.1", "socket.io-client": "4.4.1", "uuid": "^8.3.2", + "vscode-jsonrpc": "^6.0.0", "vscode-languageserver-protocol": "~3.15.3", "vscode-uri": "^2.1.1", "vscode-ws-jsonrpc": "^0.2.0", @@ -125,6 +126,15 @@ } }, "theiaExtensions": [ + { + "frontend": "lib/common/common-module", + "backend": "lib/common/common-module", + "electronMain": "lib/common/common-module" + }, + { + "backend": "lib/node/node-common-module", + "electronMain": "lib/node/node-common-module" + }, { "frontend": "lib/browser/i18n/i18n-frontend-module", "backend": "lib/node/i18n/i18n-backend-module" @@ -149,6 +159,9 @@ { "backend": "lib/node/hosting/backend-hosting-module", "backendElectron": "lib/electron-node/hosting/electron-backend-hosting-module" + }, + { + "backendElectron": "lib/electron-node/electron-node-backend-module" } ], "keywords": [ diff --git a/packages/core/src/browser/frontend-application.ts b/packages/core/src/browser/frontend-application.ts index 10043a3165dfb..6c59a83e489c5 100644 --- a/packages/core/src/browser/frontend-application.ts +++ b/packages/core/src/browser/frontend-application.ts @@ -15,8 +15,7 @@ // ***************************************************************************** import { inject, injectable, named } from 'inversify'; -import { ContributionProvider, CommandRegistry, MenuModelRegistry, isOSX, BackendStopwatch, LogLevel, Stopwatch } from '../common'; -import { MaybePromise } from '../common/types'; +import { ContributionProvider, CommandRegistry, MenuModelRegistry, isOSX, BackendStopwatch, LogLevel, Stopwatch, MaybePromise, serviceIdentifier } from '../common'; import { KeybindingRegistry } from './keybinding'; import { Widget } from './widgets'; import { ApplicationShell } from './shell/application-shell'; @@ -30,7 +29,7 @@ import { TooltipService } from './tooltip-service'; /** * Clients can implement to get a callback for contributing widgets to a shell on start. */ -export const FrontendApplicationContribution = Symbol('FrontendApplicationContribution'); +export const FrontendApplicationContribution = serviceIdentifier('FrontendApplicationContribution'); export interface FrontendApplicationContribution { /** diff --git a/packages/core/src/browser/preferences/injectable-preference-proxy.ts b/packages/core/src/browser/preferences/injectable-preference-proxy.ts index b7b28967cdc2b..b57ce36feca95 100644 --- a/packages/core/src/browser/preferences/injectable-preference-proxy.ts +++ b/packages/core/src/browser/preferences/injectable-preference-proxy.ts @@ -17,10 +17,11 @@ import { inject, injectable, postConstruct } from 'inversify'; import { PreferenceSchema } from '../../common/preferences/preference-schema'; import { Disposable, DisposableCollection, Emitter, Event, MaybePromise } from '../../common'; -import { PreferenceChangeEvent, PreferenceEventEmitter, PreferenceProxyOptions, PreferenceRetrieval } from './preference-proxy'; -import { PreferenceChange, PreferenceScope, PreferenceService } from './preference-service'; -import { OverridePreferenceName, PreferenceChangeImpl, PreferenceChanges, PreferenceProviderDataChange, PreferenceProxy } from '.'; +import { PreferenceChangeEvent, PreferenceEventEmitter, PreferenceProxy, PreferenceProxyOptions, PreferenceRetrieval } from './preference-proxy'; +import { PreferenceChange, PreferenceChangeImpl, PreferenceChanges, PreferenceScope, PreferenceService } from './preference-service'; import { JSONValue } from '@phosphor/coreutils'; +import { PreferenceProviderDataChange } from './preference-provider'; +import { OverridePreferenceName } from './preference-language-override-service'; export const PreferenceProxySchema = Symbol('PreferenceProxySchema'); export interface PreferenceProxyFactory { diff --git a/packages/core/src/browser/preferences/preference-proxy.spec.ts b/packages/core/src/browser/preferences/preference-proxy.spec.ts index 8d8585a00fe03..1ce4b41429e5d 100644 --- a/packages/core/src/browser/preferences/preference-proxy.spec.ts +++ b/packages/core/src/browser/preferences/preference-proxy.spec.ts @@ -43,7 +43,7 @@ process.on('unhandledRejection', (reason, promise) => { }); import { expect } from 'chai'; -import { PreferenceValidationService } from '.'; +import { PreferenceValidationService } from './preference-validation-service'; import { JSONValue } from '@phosphor/coreutils'; let testContainer: Container; diff --git a/packages/core/src/browser/quick-input/quick-view-service.ts b/packages/core/src/browser/quick-input/quick-view-service.ts index e5765efd9be51..e77c90cb56906 100644 --- a/packages/core/src/browser/quick-input/quick-view-service.ts +++ b/packages/core/src/browser/quick-input/quick-view-service.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { inject, injectable } from 'inversify'; -import { filterItems, QuickPickItem, QuickPicks } from '..'; +import { filterItems, QuickPickItem, QuickPicks } from '../../common/quick-pick-service'; import { CancellationToken, Disposable } from '../../common'; import { ContextKeyService } from '../context-key-service'; import { QuickAccessContribution, QuickAccessProvider, QuickAccessRegistry } from './quick-access'; diff --git a/packages/core/src/browser/saveable.ts b/packages/core/src/browser/saveable.ts index 0e0f96a4ba4a1..6a3684c3947e2 100644 --- a/packages/core/src/browser/saveable.ts +++ b/packages/core/src/browser/saveable.ts @@ -21,6 +21,7 @@ import { MaybePromise } from '../common/types'; import { Key } from './keyboard/keys'; import { AbstractDialog } from './dialogs'; import { waitForClosed } from './widgets'; +import { URI } from 'vscode-uri'; export interface Saveable { readonly dirty: boolean; @@ -67,6 +68,10 @@ export namespace Saveable { return !!arg && ('dirty' in arg) && ('onDirtyChanged' in arg); } // eslint-disable-next-line @typescript-eslint/no-explicit-any + export function isUntitled(arg: any): boolean { + return !!arg && ('uri' in arg) && URI.parse((arg as { uri: string; }).uri).scheme === 'untitled'; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any export function get(arg: any): Saveable | undefined { if (is(arg)) { return arg; diff --git a/packages/core/src/common/common-module.ts b/packages/core/src/common/common-module.ts new file mode 100644 index 0000000000000..b9f68f464b335 --- /dev/null +++ b/packages/core/src/common/common-module.ts @@ -0,0 +1,59 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from 'inversify'; +import { DeferredConnection, DeferredConnectionFactory } from './connection'; +import { DefaultConnectionMultiplexer } from './connection-multiplexer'; +import { ConnectionTransformer, TransformedConnection } from './connection-transformer'; +import { ContainerScopeFactory, ContainerScopeRegistry, DefaultContainerScope, DefaultContainerScopeRegistry } from './container-scope'; +import { JsonRpcConnection, JsonRpcConnectionFactory } from './json-rpc'; +import { LazyProxyFactory, LazyProxyHandler } from './proxy'; +import { DefaultRc, RcFactory } from './reference-counter'; +import { DefaultReflection, Reflection } from './reflection'; +import { DefaultRpcProxying, DefaultRpcProxyProvider, RpcProxying } from './rpc'; + +export default new ContainerModule(bind => { + // #region transients + bind(DefaultConnectionMultiplexer).toSelf().inTransientScope(); + bind(DefaultRpcProxyProvider).toSelf().inTransientScope(); + bind(ContainerScopeRegistry).to(DefaultContainerScopeRegistry).inTransientScope(); + // #endregion + // #region singletons + bind(RpcProxying).to(DefaultRpcProxying).inSingletonScope(); + bind(Reflection).to(DefaultReflection).inSingletonScope(); + // #endregion + // #region factories + bind(ConnectionTransformer) + .toFunction((connection, transformer) => new TransformedConnection(connection, transformer)); + bind(DeferredConnectionFactory) + .toFunction(promise => new DeferredConnection(promise)); + bind(JsonRpcConnectionFactory) + .toFunction(connection => new JsonRpcConnection(connection)); + bind(ContainerScopeFactory) + .toFunction((container, callbacks) => new DefaultContainerScope(container, callbacks)); + bind(RcFactory) + .toFunction(ref => DefaultRc.New(ref)); + bind(LazyProxyFactory) + .toDynamicValue(ctx => promise => { + // eslint-disable-next-line no-null/no-null + const nullObject = Object.freeze(Object.create(null)); + const reflection = ctx.container.get(Reflection); + const proxyHandler = new LazyProxyHandler(promise, reflection); + return new Proxy(nullObject, proxyHandler); + }) + .inSingletonScope(); + // #endregion +}); diff --git a/packages/core/src/common/connection-multiplexer.ts b/packages/core/src/common/connection-multiplexer.ts new file mode 100644 index 0000000000000..2e609d715b503 --- /dev/null +++ b/packages/core/src/common/connection-multiplexer.ts @@ -0,0 +1,225 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { injectable } from 'inversify'; +import { serviceIdentifier } from './types'; +import { + AbstractConnection, Connection, ConnectionEmitter, ConnectionEmitterHandler, ConnectionProvider, ConnectionState +} from './connection'; +import { DisposableCollection } from './disposable'; + +export const ConnectionMultiplexer = serviceIdentifier>('ConnectionMultiplexer'); +export type ConnectionMultiplexer = ConnectionProvider & ConnectionEmitter; + +/** + * @internal + * + * This default implementation follows a simple protocol to open channels: + * + * 1) Send a `[OPEN, id, params]` message to request opening a new channel. + * 2) Remote must accept or refuse the channel creation request: + * - Accept: Send `[READY, id]`. + * - Refuse: Send `[CLOSE, id]`. + * 3) Send `[MESSAGE, id, message]` to send messages to the remote channel. + * 4) Send `[CLOSE, id]` to terminate the remote channel. + * + * Both endpoints can open channels (bidirectional). Now since multiplexers on + * either side of the connection are unaware of the other's channel ID sequence, + * there is a small trick used in this implementation to avoid ID collisions: + * + * When a given endpoint opens a new channel it uses a positive integer internally + * but it should send out the negated ID to the remote. + * + * Then for each message sent we keep negating the ID from what's internally used + * so that it matches the remote's mapping. + * + * One way to see it is that each endpoint's internal map is a mirror of the other: + * `Channel(1)` on one side is `Channel(-1)` on the other side. + */ +@injectable() +export class DefaultConnectionMultiplexer implements ConnectionMultiplexer { + + protected idSequence = 1; + protected channels = new Map>(); + protected handlers: ConnectionEmitterHandler[] = []; + protected transport?: Connection; + protected disposables = new DisposableCollection(); + + initialize(connection: Connection): ConnectionMultiplexer { + this.transport = connection; + this.transport.onMessage(message => this.handleTransportMessage(message), undefined, this.disposables); + this.transport.onClose(() => this.handleTransportClosed(), undefined, this.disposables); + return this as ConnectionMultiplexer; + } + + open(params: object): Connection { + const id = this.idSequence++; + const channel = this.createChannel(id); + this.registerChannel(channel); + channel.sendOpen(params); + return channel; + } + + listen(accept: ConnectionEmitterHandler): void { + this.handlers.push(accept); + } + + protected createChannel(id: number): Channel { + return new Channel(id, message => this.sendChannelMessage(message)); + } + + protected registerChannel(channel: Channel): void { + this.channels.set(channel.id, channel); + channel.onClose(() => { + this.channels.delete(channel.id); + }); + } + + protected getChannel(id: number): Channel { + const channel = this.channels.get(id); + if (!channel) { + throw new Error(`channel not found: id=${id}`); + } + return channel; + } + + protected handleTransportMessage(message: ChannelMessage): void { + if (message[0] === ChannelMessageType.OPEN) { + const [, id, params] = message; + if (id >= 0) { + this.sendChannelMessage([ChannelMessageType.CLOSE, -id]); + throw new Error('incoming channel id should be negative'); + } + const channel = this.createChannel(id); + this.acceptChannel(channel, params).then(accept => { + if (accept) { + channel.setOpen(); + this.registerChannel(channel); + this.sendChannelMessage([ChannelMessageType.READY, -id]); + } else { + this.sendChannelMessage([ChannelMessageType.CLOSE, -id]); + } + }); + } else if (message[0] === ChannelMessageType.READY) { + const [, id] = message; + this.getChannel(id).setOpen(); + } else if (message[0] === ChannelMessageType.MESSAGE) { + const [, id, channelMessage] = message; + this.getChannel(id).emitMessage(channelMessage); + } else if (message[0] === ChannelMessageType.CLOSE) { + const [, id] = message; + this.getChannel(id).dispose(); + } else { + throw new Error(`unhandled message: ${JSON.stringify(message)}`); + } + } + + protected async acceptChannel(channel: Channel, params: any): Promise { + for (const handler of this.handlers) { + if (await new Promise(async (resolve, reject) => { + const accept = () => { + resolve(true); + return channel; + }; + const next = (error?: Error) => { + if (error) { + reject(error); + } else { + resolve(false); + } + }; + try { + await handler(params, accept, next); + } catch (error) { + console.error(error); + resolve(false); + } + })) { + return true; + } + } + return false; + } + + protected sendChannelMessage(message: ChannelMessage): void { + this.transport!.sendMessage(message); + } + + protected handleTransportClosed(): void { + this.disposables.dispose(); + this.channels.forEach(channel => channel.dispose()); + if (this.channels.size > 0) { + console.warn('some channels might be leaked!'); + } + } +} + +/** + * @internal + */ +export class Channel extends AbstractConnection { + + state = ConnectionState.OPENING; + + constructor( + public id: number, + protected sendChannelMessage: (message: ChannelMessage) => void + ) { + super(); + } + + sendOpen(params: object): void { + this.ensureState(ConnectionState.OPENING); + this.sendChannelMessage([ChannelMessageType.OPEN, -this.id, params]); + } + + setOpen(): void { + this.setOpenedAndEmit(); + } + + emitMessage(message: any): void { + this.onMessageEmitter.fire(message); + } + + sendMessage(message: any): void { + this.sendChannelMessage([ChannelMessageType.MESSAGE, -this.id, message]); + } + + close(): void { + this.sendChannelMessage([ChannelMessageType.CLOSE, -this.id]); + this.dispose(); + } + + override dispose(): void { + this.setClosedAndEmit(); + super.dispose(); + } +} + +export type ChannelMessage = + [type: ChannelMessageType.OPEN, id: number, params: object] | + [type: ChannelMessageType.READY, id: number] | + [type: ChannelMessageType.MESSAGE, id: number, message: any] | + [type: ChannelMessageType.CLOSE, id: number]; + +export enum ChannelMessageType { + OPEN, + READY, + MESSAGE, + CLOSE +} diff --git a/packages/core/src/common/connection-transformer.ts b/packages/core/src/common/connection-transformer.ts new file mode 100644 index 0000000000000..c19a468e498fc --- /dev/null +++ b/packages/core/src/common/connection-transformer.ts @@ -0,0 +1,82 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable } from 'inversify'; +import { serviceIdentifier } from './types'; +import { Connection, ConnectionState } from './connection'; +import { Emitter, Event } from './event'; +import { DisposableCollection } from './disposable'; + +export const ConnectionTransformer = serviceIdentifier('ConnectionTransformerFactory'); +export type ConnectionTransformer = (connection: Connection, transformer: MessageTransformer) => Connection; + +export interface MessageTransformer { + /** + * Called with the received message to transform before emitting it back to readers. + * + * Note that you can filter incoming messages by not calling `emit`. + */ + decode(message: From, emit: (message: To) => void): void + /** + * Called with the message to transform before sending to the underlying connection. + */ + encode(message: To, write: (message: From) => void): void +} + +@injectable() +export class TransformedConnection implements Connection { + + protected disposables = new DisposableCollection(); + protected onMessageEmitter = this.disposables.pushThru(new Emitter()); + + constructor( + protected connection: Connection, + protected transformer: MessageTransformer + ) { + this.connection.onMessage(message => { + this.transformer.decode(message, decoded => this.onMessageEmitter.fire(decoded)); + }, undefined, this.disposables); + } + + get state(): ConnectionState { + return this.connection.state; + } + + get onOpen(): Event { + return this.connection.onOpen; + } + + get onClose(): Event { + return this.connection.onClose; + } + + get onError(): Event { + return this.connection.onError; + } + + get onMessage(): Event { + return this.onMessageEmitter.event; + } + + sendMessage(message: To): void { + this.transformer.encode(message, encoded => this.connection.sendMessage(encoded)); + } + + close(): void { + this.connection.close(); + this.disposables.dispose(); + } +} diff --git a/packages/core/src/common/connection.ts b/packages/core/src/common/connection.ts new file mode 100644 index 0000000000000..5d7f3d7fcefef --- /dev/null +++ b/packages/core/src/common/connection.ts @@ -0,0 +1,270 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { injectable } from 'inversify'; +import { Disposable, DisposableCollection } from './disposable'; +import { Emitter, Event } from './event'; +import { MaybePromise, serviceIdentifier } from './types'; + +export enum ConnectionState { + OPENING, + OPENED, + CLOSING, + CLOSED, +} +export namespace ConnectionState { + /** + * @param connection + * @returns Whether `connection` is or is going to be closed. + */ + export function isClosed(connection: Connection): connection is Connection & { state: ConnectionState.CLOSING | ConnectionState.CLOSED } { + return connection.state === ConnectionState.CLOSING || connection.state === ConnectionState.CLOSED; + } +} + +/** + * Helper type to get a `Connection` without triggering the linter. + */ +export type AnyConnection = Connection; + +/** + * A `Connection` allows you to listen for messages and send messages back. + * + * Messages must arrive integrally and in the order they are sent! + * + * Most implementations are going to be API adapters. + */ +export const Connection = serviceIdentifier>('Connection'); +export interface Connection { + readonly state: ConnectionState + onOpen: Event + onClose: Event + /** + * Emitted when something goes wrong with the underlying transport. + * Typically when a message fails to be sent or received. + */ + onError: Event + onMessage: Event + sendMessage(message: T): void + close(): void +} + +@injectable() +export abstract class AbstractConnection implements Connection, Disposable { + + abstract state: ConnectionState; + abstract close(): void; + abstract sendMessage(message: T): void; + + protected disposables = new DisposableCollection(); + protected onOpenEmitter = this.disposables.pushThru(new Emitter()); + protected onCloseEmitter = this.disposables.pushThru(new Emitter()); + protected onErrorEmitter = this.disposables.pushThru(new Emitter()); + protected onMessageEmitter = this.disposables.pushThru(new Emitter()); + + get onOpen(): Event { + return this.onOpenEmitter.event; + } + + get onClose(): Event { + return this.onCloseEmitter.event; + } + + get onError(): Event { + return this.onErrorEmitter.event; + } + + get onMessage(): Event { + return this.onMessageEmitter.event; + } + + dispose(): void { + if (this.disposables.disposed) { + throw new Error('connection is already disposed'); + } + this.disposables.dispose(); + } + + /** + * @throws if {@link state} is not {@link ConnectionState.OPENING}. + */ + protected setOpenedAndEmit(): void { + this.ensureState(ConnectionState.OPENING); + this.state = ConnectionState.OPENED; + this.onOpenEmitter.fire(); + } + + /** + * @throws if {@link state} is already {@link ConnectionState.CLOSED}. + */ + protected setClosedAndEmit(): void { + this.ensureStateNot(ConnectionState.CLOSED); + this.state = ConnectionState.CLOSED; + this.onCloseEmitter.fire(); + } + + /** + * @throws if {@link state} does not match anything in {@link states}. + */ + protected ensureState(...states: ConnectionState[]): void { + if (states.every(state => this.state !== state)) { + throw new Error(`unexpected connection state: ${ConnectionState[this.state]}`); + } + } + + /** + * @throws if {@link state} matches anything in {@link states}. + */ + protected ensureStateNot(...states: ConnectionState[]): void { + if (states.some(state => this.state === state)) { + throw new Error(`unexpected connection state: ${ConnectionState[this.state]}`); + } + } +} + +/** + * Get or create outgoing connections. + */ +export const ConnectionProvider = serviceIdentifier>('ConnectionProvider'); +export interface ConnectionProvider { + open(params: P): Connection +} + +/** + * @param params + * @param handle Call this method to accept the connection and get access to it. + * @param next Let other handlers run. Pass `error` to stop. + */ +export type ConnectionEmitterHandler = (params: P, accept: () => Connection, next: (error?: Error) => void) => MaybePromise; + +/** + * Listen for incoming connections. + */ +export const ConnectionEmitter = serviceIdentifier>('ConnectionEmitter'); +export interface ConnectionEmitter { + listen(handler: ConnectionEmitterHandler): void; +} + +/** + * Create a `Connection` which will buffer messages to send until the promise resolves. + */ +export const DeferredConnectionFactory = serviceIdentifier('DeferredConnectionFactory'); +export type DeferredConnectionFactory = (connectionPromise: PromiseLike>) => Connection; + +export class DeferredConnection extends AbstractConnection { + + protected connection?: Connection; + protected messageQueue: T[] = []; + protected _interimState = ConnectionState.OPENING; + + constructor(connectionPromise: PromiseLike>) { + super(); + connectionPromise.then(connection => { + this.connection = connection; + this.connection.onOpen(() => this.onOpenEmitter.fire(), undefined, this.disposables); + this.connection.onMessage(message => this.onMessageEmitter.fire(message), undefined, this.disposables); + this.connection.onError(error => this.onErrorEmitter.fire(error), undefined, this.disposables); + this.connection.onClose(() => { + this.onCloseEmitter.fire(); + this.dispose(); + }, undefined, this.disposables); + if (this.connection.state === ConnectionState.OPENED) { + this.onOpenEmitter.fire(); + } + if (this.connection.state === ConnectionState.CLOSED) { + this.onCloseEmitter.fire(); + this.dispose(); + } else { + this.processMessageQueue(); + if (this._interimState === ConnectionState.CLOSING) { + this.connection.close(); + } + } + }); + } + + get state(): ConnectionState { + return this.connection?.state ?? this._interimState; + } + + sendMessage(message: T): void { + if (this.connection) { + this.connection.sendMessage(message); + } else { + this.messageQueue.push(message); + } + } + + close(): void { + this._interimState = ConnectionState.CLOSING; + if (this.connection) { + this.connection.close(); + } + } + + protected processMessageQueue(): void { + this.messageQueue.forEach(message => this.connection!.sendMessage(message)); + this.messageQueue.length = 0; + } +} + +/** + * Sometimes we may use connections that didn't require an initial request to + * connect the two peers. In such a scenario, one peer might start using the + * connection before the remote peer started listening (timing issue). + * + * This method runs a small handshake protocol on {@link connection} to make + * sure that both peers are listening before running other protocols. + * + * _e.g. Using Node's fork IPC channel: the channel is established as the + * process is forked, but listeners are only eventually attached once the + * running code initializes itself asynchronously._ + */ +export function waitForRemote(connection: Connection): Promise> { + return new Promise((resolve, reject) => { + const disposables = new DisposableCollection(); + connection.onClose(() => { + disposables.dispose(); + reject(new Error('connection closed')); + }, undefined, disposables); + let received_ping_once = false; + connection.onMessage((message: PingMessage) => { + if (message === PingMessage.PING) { + if (!received_ping_once) { + received_ping_once = true; + // Resend ping in case our initial ping wasn't received. + // (e.g. the remote peer wasn't listening yet.) + // If it was received, this second ping will be ignored. + connection.sendMessage(PingMessage.PING); + connection.sendMessage(PingMessage.PONG); + } + } else if (message === PingMessage.PONG) { + resolve(connection); + disposables.dispose(); + } else { + console.warn('ping/pong: unexpected message:', message); + } + }, undefined, disposables); + connection.sendMessage(PingMessage.PING); + }); +} + +export enum PingMessage { + PING = 'ping', + PONG = 'pong' +} diff --git a/packages/core/src/common/container-scope.ts b/packages/core/src/common/container-scope.ts new file mode 100644 index 0000000000000..637621446f15e --- /dev/null +++ b/packages/core/src/common/container-scope.ts @@ -0,0 +1,122 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { inject, injectable, interfaces } from 'inversify'; +import { Disposable, DisposableCollection } from './disposable'; +import { serviceIdentifier } from './types'; +import { Rc, RcFactory } from './reference-counter'; + +/** + * This is essentially a disposable wrapper around an Inversify `Container`. + */ +export interface ContainerScope extends Disposable { + /** + * @throws if disposed. + */ + container(): interfaces.Container; + /** + * @throws if disposed. + */ + dispose(): void +} + +export const ContainerScopeFactory = serviceIdentifier('ContainerScopeFactory'); +export type ContainerScopeFactory = (container: interfaces.Container, callbacks?: ContainerScopeReady[]) => ContainerScope; + +/** + * Callback to run once a {@link ContainerScope} is ready. + * + * Binding callbacks to this symbol will only work if part of a {@link ContainerScope}. + */ +export const ContainerScopeReady = serviceIdentifier('ContainerScopeReady'); +export type ContainerScopeReady = (container: interfaces.Container) => Disposable | null | undefined | void; + +export const ContainerScopeRegistry = serviceIdentifier('ContainerScopeRegistry'); +export interface ContainerScopeRegistry { + getOrCreateScope(id: any): Rc +} + +/** + * @internal + */ +export class DefaultContainerScope implements ContainerScope { + + protected _container?: interfaces.Container; + protected disposables = new DisposableCollection(); + + constructor(container: interfaces.Container, callbacks: ContainerScopeReady[] = []) { + this._container = container; + callbacks.forEach(callback => { + const disposable = callback(this._container!); + if (Disposable.is(disposable)) { + this.disposables.push(disposable); + } + }); + } + + container(): interfaces.Container { + if (!this._container) { + throw new Error('container is disposed!'); + } + return this._container; + } + + dispose(): void { + const container = this.container(); + this._container = undefined; + this.disposables.dispose(); + container.unbindAll(); + } +} + +/** + * @internal + */ +@injectable() +export class DefaultContainerScopeRegistry implements ContainerScopeRegistry { + + protected scopes = new Map>(); + protected containerFactory: () => interfaces.Container; + + @inject(RcFactory) + protected rcFactory: RcFactory; + + @inject(ContainerScopeFactory) + protected containerScopeFactory: ContainerScopeFactory; + + initialize(containerFactory: () => interfaces.Container): ContainerScopeRegistry { + this.containerFactory = containerFactory; + return this; + } + + getOrCreateScope(id: any): Rc { + let rc = this.scopes.get(id); + if (rc) { + return rc.clone(); + } else { + const container = this.containerFactory(); + const containerScope = this.containerScopeFactory(container); + rc = this.rcFactory(containerScope); + this.scopes.set(id, rc); + rc.onWillDisposeRef(() => { + this.scopes.delete(id); + }); + return rc; + } + } +} diff --git a/packages/core/src/common/contribution-filter/contribution-filter.ts b/packages/core/src/common/contribution-filter/contribution-filter.ts index e7d145ff4df81..1e42a89076e67 100644 --- a/packages/core/src/common/contribution-filter/contribution-filter.ts +++ b/packages/core/src/common/contribution-filter/contribution-filter.ts @@ -15,12 +15,13 @@ // ***************************************************************************** import { interfaces } from 'inversify'; +import { serviceIdentifier } from '../types'; import { Filter } from './filter'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ContributionType = interfaces.ServiceIdentifier; -export const ContributionFilterRegistry = Symbol('ContributionFilterRegistry'); +export const ContributionFilterRegistry = serviceIdentifier('ContributionFilterRegistry'); export interface ContributionFilterRegistry { /** diff --git a/packages/core/src/common/contribution-provider.ts b/packages/core/src/common/contribution-provider.ts index a70c59c857cf2..0af27d3832caf 100644 --- a/packages/core/src/common/contribution-provider.ts +++ b/packages/core/src/common/contribution-provider.ts @@ -16,6 +16,7 @@ import { interfaces } from 'inversify'; import { ContributionFilterRegistry } from './contribution-filter'; +import { getOptional } from './inversify-utils'; export const ContributionProvider = Symbol('ContributionProvider'); @@ -38,27 +39,23 @@ class ContainerBasedContributionProvider implements Contributi getContributions(recursive?: boolean): T[] { if (this.services === undefined) { - const currentServices: T[] = []; - let filterRegistry: ContributionFilterRegistry | undefined; - let currentContainer: interfaces.Container | null = this.container; + const filterRegistry = getOptional(this.container, ContributionFilterRegistry); + let services: T[] = []; // eslint-disable-next-line no-null/no-null - while (currentContainer !== null) { - if (currentContainer.isBound(this.serviceIdentifier)) { + let current: interfaces.Container | null = this.container; + do { + if (current.isBound(this.serviceIdentifier)) { try { - currentServices.push(...currentContainer.getAll(this.serviceIdentifier)); + services = services.concat(current.getAll(this.serviceIdentifier)); } catch (error) { console.error(error); } } - if (filterRegistry === undefined && currentContainer.isBound(ContributionFilterRegistry)) { - filterRegistry = currentContainer.get(ContributionFilterRegistry); - } + } while ( // eslint-disable-next-line no-null/no-null - currentContainer = recursive === true ? currentContainer.parent : null; - } - - this.services = filterRegistry ? filterRegistry.applyFilters(currentServices, this.serviceIdentifier) : currentServices; - + current = recursive ? current.parent : null + ); + this.services = filterRegistry?.applyFilters(services, this.serviceIdentifier) ?? services; } return this.services; } diff --git a/packages/core/src/common/disposable-proxy.ts b/packages/core/src/common/disposable-proxy.ts new file mode 100644 index 0000000000000..097a99b88f6ac --- /dev/null +++ b/packages/core/src/common/disposable-proxy.ts @@ -0,0 +1,103 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Disposable } from './disposable'; + +export function createDisposableProxy(target: T): T { + return new Proxy(target, new DisposableProxyHandler()); +} + +export class DisposableProxyHandler implements ProxyHandler { + + protected disposed = false; + + apply(target: T, thisArg: any, argArray: any[]): any { + return Reflect.apply(target as any, thisArg, argArray); + } + + // construct not supported on Disposable + + defineProperty(target: T, p: string | symbol, attributes: PropertyDescriptor): boolean { + this.checkDisposed(target); + return Reflect.defineProperty(target, p, attributes); + } + + deleteProperty(target: T, p: string | symbol): boolean { + this.checkDisposed(target); + return Reflect.deleteProperty(target, p); + } + + get(target: T, p: string | symbol, receiver: any): any { + if (p === 'dispose') { + return () => { + if (!this.checkDisposed(target)) { + this.disposed = true; + } + }; + } + this.checkDisposed(target); + return Reflect.get(target, p, receiver); + } + + getOwnPropertyDescriptor(target: T, p: string | symbol): PropertyDescriptor | undefined { + this.checkDisposed(target); + return Reflect.getOwnPropertyDescriptor(target, p); + } + + getPrototypeOf(target: T): object | null { + this.checkDisposed(target); + return Reflect.getPrototypeOf(target); + } + + has(target: T, p: string | symbol): boolean { + this.checkDisposed(target); + return Reflect.has(target, p); + } + + isExtensible(target: T): boolean { + this.checkDisposed(target); + return Reflect.isExtensible(target); + } + + ownKeys(target: T): ArrayLike { + this.checkDisposed(target); + return Reflect.ownKeys(target); + } + + preventExtensions(target: T): boolean { + this.checkDisposed(target); + return Reflect.preventExtensions(target); + } + + set(target: T, p: string | symbol, value: any, receiver: any): boolean { + this.checkDisposed(target); + return Reflect.set(target, p, value, receiver); + } + + setPrototypeOf(target: T, v: object): boolean { + this.checkDisposed(target); + return Reflect.setPrototypeOf(target, v); + } + + protected checkDisposed(target: T): boolean { + if (this.disposed) { + console.trace('using a disposed instance!', target); + } + return this.disposed; + } +} diff --git a/packages/core/src/common/disposable.ts b/packages/core/src/common/disposable.ts index 9a7d8bfe1f861..b4ca57f2a718d 100644 --- a/packages/core/src/common/disposable.ts +++ b/packages/core/src/common/disposable.ts @@ -15,9 +15,21 @@ // ***************************************************************************** import { Event, Emitter } from './event'; +/** + * The disposable pattern is helpful when dealing with resource allocations. + * There are no destructors in TypeScript/JavaScript, and even though objects + * are garbage collected you cannot listen for that event. + * + * Instead whenever an object handles resources that should be cleaned, it + * may implement `Disposable` so that its "owner" can call this method + * when the instance and its dependencies are no longer required. + * + * Note that `Disposable` instances should only be disposed once, it is + * undefined behavior to dispose the same instance multiple times. + */ export interface Disposable { /** - * Dispose this object. + * Dispose resources allocated by this object. */ dispose(): void; } @@ -48,11 +60,20 @@ Object.defineProperty(Disposable, 'NULL', { export class DisposableCollection implements Disposable { - protected readonly disposables: Disposable[] = []; - protected readonly onDisposeEmitter = new Emitter(); + protected disposables: Disposable[] = []; + protected onDisposeEmitter = new Emitter(); + + /** + * Internal flag set when processing disposables. + */ + private disposingElements = false; + + constructor(...disposables: Disposable[]) { + this.pushAll(disposables); + } - constructor(...toDispose: Disposable[]) { - toDispose.forEach(d => this.push(d)); + get disposed(): boolean { + return this.disposables.length === 0; } /** @@ -63,18 +84,6 @@ export class DisposableCollection implements Disposable { return this.onDisposeEmitter.event; } - protected checkDisposed(): void { - if (this.disposed && !this.disposingElements) { - this.onDisposeEmitter.fire(undefined); - this.onDisposeEmitter.dispose(); - } - } - - get disposed(): boolean { - return this.disposables.length === 0; - } - - private disposingElements = false; dispose(): void { if (this.disposed || this.disposingElements) { return; @@ -116,6 +125,24 @@ export class DisposableCollection implements Disposable { ); } + /** + * Pass-through method to register a disposable while passing it back to + * the caller. + * + * @param disposable + * @returns The passed `disposable`. + */ + pushThru(disposable: T): T { + this.push(disposable); + return disposable; + } + + protected checkDisposed(): void { + if (this.disposed && !this.disposingElements) { + this.onDisposeEmitter.fire(undefined); + this.onDisposeEmitter.dispose(); + } + } } export type DisposableGroup = { push(disposable: Disposable): void } | { add(disposable: Disposable): void }; diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 5c944c157087a..b351c911d3e29 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -42,5 +42,15 @@ export * from './nls'; export * from './numbers'; export * from './performance'; -import { environment } from '@theia/application-package/lib/environment'; -export { environment }; +export { environment } from '@theia/application-package/lib/environment'; + +export { Deferred, delay, retry, timeout, timeoutReject, wait, waitForEvent } from './promise-util'; +export { ProxyProvider, LazyProxyFactory } from './proxy'; +export { AnyConnection, AbstractConnection, Connection, ConnectionEmitter, ConnectionProvider, ConnectionState, DeferredConnectionFactory } from './connection'; +export { JsonRpcConnectionFactory } from './json-rpc'; +export { bindServiceProvider, servicePath, ServicePath, ServiceProvider, ServiceContribution } from './service-provider'; +export { Reflection } from './reflection'; +export { RpcConnection, RpcProxying } from './rpc'; +export { ConnectionTransformer, MessageTransformer } from './connection-transformer'; +export { ConnectionMultiplexer } from './connection-multiplexer'; +export { ContainerScope, ContainerScopeReady } from './container-scope'; diff --git a/packages/core/src/common/inversify-utils.ts b/packages/core/src/common/inversify-utils.ts new file mode 100644 index 0000000000000..52cf2e56130fc --- /dev/null +++ b/packages/core/src/common/inversify-utils.ts @@ -0,0 +1,48 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import type { interfaces } from 'inversify'; + +export function getOptional(container: interfaces.Container, serviceIdentifier: interfaces.ServiceIdentifier): T | undefined { + if (container.isBound(serviceIdentifier)) { + return container.get(serviceIdentifier); + } +} + +export function getAllOptional(container: interfaces.Container, serviceIdentifier: interfaces.ServiceIdentifier): T[] { + if (container.isBound(serviceIdentifier)) { + return container.getAll(serviceIdentifier); + } + return []; +} + +export function getAllNamedOptional(container: interfaces.Container, serviceIdentifier: interfaces.ServiceIdentifier, name: string | number | symbol): T[] { + if (container.isBoundNamed(serviceIdentifier, name)) { + return container.getAllNamed(serviceIdentifier, name); + } + return []; +} + +export function collectRecursive(container: interfaces.Container, collect: (container: interfaces.Container) => T[]): T[] { + let result: T[] = []; + let current: interfaces.Container | null = container; + do { + result = result.concat(collect(current)); + } while ( + current = current.parent + ); + return result; +} diff --git a/packages/core/src/common/json-rpc.ts b/packages/core/src/common/json-rpc.ts new file mode 100644 index 0000000000000..7eb802d7bff41 --- /dev/null +++ b/packages/core/src/common/json-rpc.ts @@ -0,0 +1,98 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as jsonrpc from 'vscode-jsonrpc'; +import { CancellationToken } from './cancellation'; +import { Disposable } from './disposable'; +import { RpcConnection } from './rpc'; +import { Connection } from './connection'; +import { serviceIdentifier } from './types'; + +/** + * Create a `RpcConnection` from a `Connection` using the JSON-RPC protocol. + */ +export const JsonRpcConnectionFactory = serviceIdentifier('JsonRpcConnectionFactory'); +export type JsonRpcConnectionFactory = (connection: Connection) => RpcConnection; + +export class JsonRpcConnection implements RpcConnection { + + protected messageConnection: jsonrpc.MessageConnection; + + constructor(connection: Connection) { + const reader = this.createReader(connection); + const writer = this.createWriter(connection); + this.messageConnection = jsonrpc.createMessageConnection(reader, writer); + this.messageConnection.listen(); + } + + onClose(handler: () => void, thisArg: unknown): Disposable { + return this.messageConnection.onClose(handler, thisArg); + } + + onNotification(handler: (method: string, params: any[]) => void): void { + this.messageConnection.onNotification((method, params) => { + if (params === undefined) { + handler(method, []); + } else if (Array.isArray(params)) { + handler(method, params); + } else { + throw new TypeError('received a JSON-RPC params as an object'); + } + }); + } + + onRequest(handler: (method: string, params: any[], token: CancellationToken) => any): void { + this.messageConnection.onRequest((method, params, token) => { + if (params === undefined) { + return handler(method, [], token); + } else if (Array.isArray(params)) { + return handler(method, params, token); + } else { + throw new TypeError('received a JSON-RPC params as an object'); + } + }); + } + + sendNotification(method: string, params: any[]): void { + this.messageConnection.sendNotification(method, jsonrpc.ParameterStructures.byPosition, ...params); + } + + sendRequest(method: string, params: any[]): Promise { + return this.messageConnection.sendRequest(method, jsonrpc.ParameterStructures.byPosition, ...params); + } + + protected createReader(connection: Connection): jsonrpc.MessageReader { + return { + dispose: () => { }, + listen: callback => connection.onMessage(message => callback(message as jsonrpc.Message)), + onClose: listener => connection.onClose(() => listener()), + onError: () => Disposable.NULL, + onPartialMessage: () => Disposable.NULL, + }; + } + + protected createWriter(connection: Connection): jsonrpc.MessageWriter { + return { + dispose: () => { }, + onClose: listener => connection.onClose(() => listener()), + onError: () => Disposable.NULL, + write: async message => connection.sendMessage(message), + end: () => { } + }; + } +} diff --git a/packages/core/src/electron-main/event-utils.ts b/packages/core/src/common/node-event-utils.ts similarity index 50% rename from packages/core/src/electron-main/event-utils.ts rename to packages/core/src/common/node-event-utils.ts index fd6a7914c4072..97154b7f7e6f6 100644 --- a/packages/core/src/electron-main/event-utils.ts +++ b/packages/core/src/common/node-event-utils.ts @@ -16,21 +16,23 @@ import { Disposable, DisposableCollection } from '../common'; -/** - * @param collection If a collection is passed in, the new disposable is added to that collection. Otherwise, the new disposable is returned. - */ -export function createDisposableListener( - emitter: NodeJS.EventEmitter, signal: string, handler: (event: K, ...args: unknown[]) => unknown, collection: DisposableCollection -): void; -export function createDisposableListener(emitter: NodeJS.EventEmitter, signal: string, handler: (event: K, ...args: unknown[]) => unknown): Disposable; -export function createDisposableListener( - emitter: NodeJS.EventEmitter, signal: string, handler: (event: K, ...args: unknown[]) => unknown, collection?: DisposableCollection -): Disposable | void { +export type EventNames = Parameters[0]; +export type Listener = EventNames> = Parameters[0] extends K ? Parameters[1] : never; + +export function createDisposableListener( + emitter: NodeJS.EventEmitter, + signal: string, + handler: (...args: T) => void +): Disposable { emitter.on(signal, handler); - const disposable = Disposable.create(() => { try { emitter.off(signal, handler); } catch { } }); - if (collection) { - collection.push(disposable); - } else { - return disposable; - } + return Disposable.create(() => { try { emitter.off(signal, handler); } catch { } }); +} + +export function pushDisposableListener( + collection: DisposableCollection, + emitter: NodeJS.EventEmitter, + signal: string, + handler: (...args: T) => void +): void { + collection.push(createDisposableListener(emitter, signal, handler)); } diff --git a/packages/core/src/common/proxy.ts b/packages/core/src/common/proxy.ts new file mode 100644 index 0000000000000..e5bc2cbd64b42 --- /dev/null +++ b/packages/core/src/common/proxy.ts @@ -0,0 +1,86 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Emitter, Event } from './event'; +import { Reflection } from './reflection'; +import { MaybePromise, serviceIdentifier } from './types'; +import { ServicePath } from './service-provider'; + +export type Proxyable = { + [key in string]?: Event | ((...args: any[]) => MaybePromise) +}; + +export type EventNames = { [K in keyof T]: T[K] extends Event ? K : never }[keyof T]; +export type MethodNames = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]; + +export type Proxied = { + readonly [K in EventNames | MethodNames]: + T[K] extends Event + ? T[K] + : T[K] extends (...params: infer ParametersType) => infer ReturnType + ? ReturnType extends PromiseLike + ? (...params: ParametersType) => Promise + : (...params: ParametersType) => Promise + : never; +}; + +export const ProxyProvider = serviceIdentifier('ProxyProvider'); +export interface ProxyProvider { + getProxy(serviceId: string | ServicePath, params?: P): Proxied; +} + +/** + * A `LazyProxy` allows you to get a reference to an instance that will delay method calls + * until the promise to the actual instance resolves. + */ +export const LazyProxyFactory = serviceIdentifier('LazyProxyFactory'); +export type LazyProxyFactory = (promise: PromiseLike) => Proxied; + +export class LazyProxyHandler implements ProxyHandler { + + protected emitters = new Map>(); + protected cache = new Map | Function>(); + + constructor( + protected promiseLike: PromiseLike, + protected reflection: Reflection, + ) { + this.promiseLike.then(instance => { + this.reflection.getEventNames(instance).forEach(event => { + instance[event]!(arg => this.emitters.get(event)?.fire(arg)); + }); + }); + } + + get(target: T, property: string | symbol, receiver: unknown): any { + if (typeof property !== 'string') { + throw new Error('you can only index this proxy with strings'); + } + let returnValue = this.cache.get(property); + if (!returnValue) { + if (this.reflection.isEventName(property)) { + const emitter = new Emitter(); + this.emitters.set(property, emitter); + returnValue = emitter.event; + } else { + returnValue = async (...args: unknown[]): Promise => this.promiseLike.then(instance => (instance[property] as Function)(...args)); + } + } + return returnValue; + } +} diff --git a/packages/core/src/common/reference-counter.ts b/packages/core/src/common/reference-counter.ts new file mode 100644 index 0000000000000..c6f222f846d14 --- /dev/null +++ b/packages/core/src/common/reference-counter.ts @@ -0,0 +1,134 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Disposable } from './disposable'; +import { Emitter, Event } from './event'; +import { serviceIdentifier } from './types'; + +export const RcFactory = serviceIdentifier('RcFactory'); +export type RcFactory = (ref: T) => Rc; + +/** + * This is a disposable reference counter to some underlying disposable instance. + * + * Call `.clone()` to create a new reference (increasing the reference count). + * + * The underlying resource will be disposed once all references are disposed. + */ +export interface Rc extends Disposable { + ref(): Omit; + /** + * Create a new `Rc` instance referencing the same `T` instance, + * incrementing the reference count by 1. + * @throws if `T` was disposed. + */ + clone(): Rc + /** + * Decrement the reference count by 1. + * + * Note that calling `dispose` more than once does nothing. + */ + dispose(): void + /** + * Is this specific `Rc` instance disposed. + */ + isDisposed(): boolean + /** + * Is `T` disposed. If `true` it means all `Rc` were also disposed. + */ + isRefDisposed(): this is never + /** + * Event emitted once all `Rc` are disposed and we are about to dispose + * the underlying `T` instance. + */ + onWillDisposeRef: Event> +}; + +/** + * @internal + */ +export interface DefaultRcState { + onWillDisposeReferenceEmitter: Emitter + count: number + ref?: T +} +/** + * @internal + */ +export class DefaultRc implements Rc { + + static New(ref: T): Rc { + return new this({ + ref, + count: 0, + onWillDisposeReferenceEmitter: new Emitter() + }); + } + + protected _disposed = false; + + protected constructor( + protected _state: DefaultRcState + ) { + this._state.count += 1; + } + + get onWillDisposeRef(): Event { + if (this.isRefDisposed()) { + return Event.None; + } + return this._state.onWillDisposeReferenceEmitter.event; + } + + ref(): T { + this.checkDisposed(); + return this._state.ref!; + } + + clone(): Rc { + this.checkDisposed(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new (this.constructor as any)(this._state!); + } + + dispose(): void { + if (this.isDisposed()) { + console.trace('reference counter is already disposed!'); + return; + } + this._state.count -= 1; + if (this._state.count === 0) { + this._state.onWillDisposeReferenceEmitter.fire(this._state.ref!); + this._state.onWillDisposeReferenceEmitter.dispose(); + this._state.ref!.dispose(); + this._state.ref = undefined; + } + } + + isDisposed(): this is never { + return this._disposed; + } + + isRefDisposed(): this is never { + return this._state.ref === undefined; + } + + protected checkDisposed(): void { + if (this.isDisposed()) { + throw new Error('reference is disposed!'); + } + } +} diff --git a/packages/core/src/common/reflection.ts b/packages/core/src/common/reflection.ts new file mode 100644 index 0000000000000..9eed5e899c4d8 --- /dev/null +++ b/packages/core/src/common/reflection.ts @@ -0,0 +1,60 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { injectable } from 'inversify'; +import { serviceIdentifier } from './types'; + +export const Reflection = serviceIdentifier('Reflection'); +export interface Reflection { + /** + * Based solely on the property name return if it maps to an `Event`. + */ + isEventName(name: string): boolean + /** + * Return all enumerable properties that appear to be events based on the name. + * @param target instance or prototype to reflect on. + */ + getEventNames(target: object): Set +} + +@injectable() +export class DefaultReflection implements Reflection { + + isEventName(name: string): boolean { + return /^on[A-Z]/.test(name); + } + + getEventNames(instance: object): Set { + if (typeof instance !== 'object') { + throw new TypeError('instance is not an object!'); + } + // Start with the passed instance, then recursively get methods of parent prototypes + const events = new Set(); + let current: object | null = instance; + do { + for (const property of Object.getOwnPropertyNames(current)) { + if (this.isEventName(property) && typeof (instance as any)[property] === 'function') { + events.add(property); + } + } + } while ( + current = Object.getPrototypeOf(current) + ); + return events; + } +} diff --git a/packages/core/src/common/rpc.ts b/packages/core/src/common/rpc.ts new file mode 100644 index 0000000000000..7931539c0eca0 --- /dev/null +++ b/packages/core/src/common/rpc.ts @@ -0,0 +1,140 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { inject, injectable } from 'inversify'; +import { CancellationToken } from './cancellation'; +import { Emitter, Event } from './event'; +import { Proxied, ProxyProvider } from './proxy'; +import { Reflection } from './reflection'; +import { serviceIdentifier } from './types'; + +/** + * Represents a scoped connection to a remote service on which to call methods. + * + * There should be a 1-to-1 relationship between a `RpcConnection` and the + * remote service it represents. + */ +export const RpcConnection = serviceIdentifier('RpcConnection'); +export interface RpcConnection { + onClose: Event + onRequest(handler: (method: string, params: any[], token: CancellationToken) => any): void + onNotification(handler: (method: string, params: any[]) => void): void + sendRequest(method: string, params: any[]): Promise + sendNotification(method: string, params: any[]): void +} + +/** + * Methods to wire JavaScript {@link Proxy} instances over {@link RpcConnection}s. + */ +export const RpcProxying = serviceIdentifier('RpcProxying'); +export interface RpcProxying { + createProxy(rpcConnection: RpcConnection): Proxied + serve(server: object, rpcConnection: RpcConnection): void +} + +/** + * @internal + */ +@injectable() +export class DefaultRpcProxying implements RpcProxying { + + @inject(Reflection) + protected reflection: Reflection; + + createProxy(rpcConnection: RpcConnection): Proxied { + // eslint-disable-next-line no-null/no-null + const emptyObject = Object.freeze(Object.create(null)); + const rpcProxyHandler = new RpcProxyHandler(rpcConnection, this.reflection); + return new Proxy(emptyObject, rpcProxyHandler); + } + + serve(server: any, rpcConnection: RpcConnection): void { + rpcConnection.onRequest((method, params, token) => server[method](...params, token)); + this.reflection.getEventNames(server).forEach( + eventName => server[eventName]((event: unknown) => rpcConnection.sendNotification(eventName, [event])) + ); + } +} + +/** + * @internal + */ +export type RpcConnectionProvider = (serviceId: string, serviceParams?: any) => RpcConnection; + +/** + * @internal + */ +@injectable() +export class DefaultRpcProxyProvider implements ProxyProvider { + + protected rpcConnectionProvider?: RpcConnectionProvider; + protected connectionToProxyCache = new WeakMap(); + + @inject(RpcProxying) + protected rpcProxying: RpcProxying; + + initialize(rpcConnectionProvider: RpcConnectionProvider): ProxyProvider { + this.rpcConnectionProvider = rpcConnectionProvider; + return this; + } + + getProxy(serviceId: string, params?: any): any { + const rpcConnection = this.rpcConnectionProvider!(serviceId, params); + let proxy = this.connectionToProxyCache.get(rpcConnection); + if (!proxy) { + this.connectionToProxyCache.set(rpcConnection, proxy = this.rpcProxying.createProxy(rpcConnection)); + } + return proxy; + } +} + +/** + * @internal + */ +export class RpcProxyHandler implements ProxyHandler { + + protected emitters = new Map(); + protected cache = new Map(); + + constructor( + protected rpcConnection: RpcConnection, + protected reflection: Reflection, + ) { + rpcConnection.onNotification((eventName, params) => { + this.emitters.get(eventName)?.fire(params[0]); + }); + } + + get(target: T, property: string | symbol, receiver: T): any { + if (typeof property !== 'string') { + throw new Error('you can only index this proxy with strings'); + } + let returnValue = this.cache.get(property); + if (!returnValue) { + if (this.reflection.isEventName(property)) { + const emitter = new Emitter(); + this.emitters.set(property, emitter); + returnValue = emitter.event; + } else { + returnValue = async (...params: any[]) => this.rpcConnection.sendRequest(property, params); + } + this.cache.set(property, returnValue); + } + return returnValue; + } +} diff --git a/packages/core/src/common/service-provider.ts b/packages/core/src/common/service-provider.ts new file mode 100644 index 0000000000000..4ca26c867e2d7 --- /dev/null +++ b/packages/core/src/common/service-provider.ts @@ -0,0 +1,128 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable, interfaces } from 'inversify'; +import { ContributionFilterRegistry } from './contribution-filter'; +import { collectRecursive, getAllNamedOptional, getOptional } from './inversify-utils'; +import { Proxyable } from './proxy'; +import { serviceIdentifier } from './types'; + +/** + * Represents the `serviceId` string referencing a given service type `T`. + */ +export type ServicePath = string & { __staticType: T }; +export function servicePath(path: string): ServicePath { + // @ts-expect-error + return path; +} + +/** + * Part of Theia's Service Layer. + * + * Whenever a remote wants to use a service over RPC, a request will go through the `ServiceProvider` to find the instance to proxy. + */ +export const ServiceProvider = serviceIdentifier('ServiceProvider'); +export interface ServiceProvider { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getService(serviceId: string | ServicePath, params?: any): T | undefined; +} + +export function bindServiceProvider(bind: interfaces.Bind, contributionName: string | number | symbol): void { + bind(ServiceProvider) + .toDynamicValue(ctx => { + const contributionFilter = getOptional(ctx.container, ContributionFilterRegistry); + const contributions = collectRecursive(ctx.container, container => getAllNamedOptional(container, ServiceContribution, contributionName)); + return new DefaultServiceProvider(contributionFilter?.applyFilters(contributions, ServiceContribution) ?? contributions); + }) + .inSingletonScope() + .whenTargetNamed(contributionName); +} + +/** + * Part of Theia's Service Layer. + * + * Requested services to offer over RPC are fetched through a `ServiceProvider` that will source from `ServiceContribution` bindings. + * + * ## Usage Examples + * + * ### Record + * + * ```ts + * bind(ServiceContribution) + * .toDynamicValue(ctx => ({ + * [PATH1]: () => ctx.container.get(Service1), + * [PATH2]: () => ctx.container.get(Service2), + * [PATH3]: params => ctx.container.get(params.yourParam ? Service3 : Service4); + * // ... + * })) + * .inSingletonScope() + * .whenTargetNamed(YourServiceNamespace); + * ``` + * + * ### Function + * + * ```ts + * bind(ServiceContribution) + * .toDynamicValue(ctx => (serviceId, params) => { + * // process arguments... + * return resolvedServiceOrUndefined; + * })) + * .inSingletonScope() + * .whenTargetNamed(YourServiceNamespace); + * ``` + */ +export const ServiceContribution = serviceIdentifier('ServiceContribution'); +export type ServiceContribution = ServiceContributionFunction | ServiceContributionRecord; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ServiceContributionFunction = (serviceId: string, params?: any) => any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface ServiceContributionRecord { [serviceId: string]: (params?: any) => any }; + +/** + * @internal + * + * This implementation dispatches a service request to its service contributions. + */ +@injectable() +export class DefaultServiceProvider implements ServiceProvider { + + constructor( + protected serviceContributions: ServiceContribution[] + ) { } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getService(serviceId: string, params?: any): T | undefined { + for (const contribution of this.serviceContributions) { + try { + let service: Proxyable | undefined; + if (typeof contribution === 'function') { + service = contribution(serviceId, params); + } else if (typeof contribution === 'object' && !Array.isArray(contribution)) { + service = contribution[serviceId]?.(params); + } else { + console.error(`unexpected contribution type: ${typeof contribution}`); + continue; + } + if (service) { + return service as T; + } + } catch (error) { + console.error(error); + } + } + throw new Error(`no service found for "${serviceId}"`); + } +} diff --git a/packages/core/src/common/types.ts b/packages/core/src/common/types.ts index 9f7710b316733..b2a95fb30e762 100644 --- a/packages/core/src/common/types.ts +++ b/packages/core/src/common/types.ts @@ -14,6 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +import type { interfaces } from 'inversify'; + export type Mutable = { -readonly [P in keyof T]: T[P] }; export type MaybeNull = { [P in keyof T]: T[P] | null }; export type MaybeUndefined = { [P in keyof T]: T[P] | undefined }; @@ -130,3 +132,25 @@ export namespace ArrayUtils { export function unreachable(_never: never, message: string = 'unhandled case'): never { throw new Error(message); } + +/** + * @internal + * + * Potentially empty Theia contribution types can cause TypeScript to complain + * if extra methods are defined. This type prevents the issue. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type EmptyMeansAny = {} extends T ? T & { [key in string | symbol]: any } : T; + +/** + * @internal + */ +export type SymbolIdentifier = symbol & interfaces.Abstract>; + +/** + * Use this to create "typed symbols" for Inversify typings to understand our bindings. + */ +export function serviceIdentifier(name: string): SymbolIdentifier { + // @ts-expect-error We won't implement `Abstract` as it is solely for static typing. + return Symbol(name); +} diff --git a/packages/core/src/common/uri-command-handler.spec.ts b/packages/core/src/common/uri-command-handler.spec.ts index be8f5630a2d14..df1be4e9b0917 100644 --- a/packages/core/src/common/uri-command-handler.spec.ts +++ b/packages/core/src/common/uri-command-handler.spec.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import * as chai from 'chai'; -import { SelectionService } from '.'; +import { SelectionService } from './selection-service'; import { MaybeArray } from './types'; import URI from './uri'; import { UriAwareCommandHandler, UriCommandHandler } from './uri-command-handler'; diff --git a/packages/core/src/common/uri-command-handler.ts b/packages/core/src/common/uri-command-handler.ts index a5c523209f7ac..400f8a126d104 100644 --- a/packages/core/src/common/uri-command-handler.ts +++ b/packages/core/src/common/uri-command-handler.ts @@ -19,7 +19,7 @@ import { SelectionService } from '../common/selection-service'; import { UriSelection } from '../common/selection'; import { CommandHandler } from './command'; -import { MaybeArray } from '.'; +import { MaybeArray } from './types'; import URI from './uri'; export interface UriCommandHandler> extends CommandHandler { @@ -145,4 +145,3 @@ export namespace UriAwareCommandHandler { return new UriAwareCommandHandler(selectionService, handler, { multi: true }); } } - diff --git a/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts b/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts deleted file mode 100644 index b3d8ce8ab9415..0000000000000 --- a/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts +++ /dev/null @@ -1,50 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2018 TypeFox and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0. -// -// This Source Code may also be made available under the following Secondary -// Licenses when the conditions for such availability set forth in the Eclipse -// Public License v. 2.0 are satisfied: GNU General Public License, version 2 -// with the GNU Classpath Exception which is available at -// https://www.gnu.org/software/classpath/license.html. -// -// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 -// ***************************************************************************** - -import { Event as ElectronEvent, ipcRenderer } from '@theia/electron/shared/electron'; -import { injectable, interfaces } from 'inversify'; -import { JsonRpcProxy } from '../../common/messaging'; -import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; -import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; -import { THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../../electron-common/messaging/electron-connection-handler'; - -export interface ElectronIpcOptions { -} - -/** - * Connection provider between the Theia frontend and the electron-main process via IPC. - */ -@injectable() -export class ElectronIpcConnectionProvider extends AbstractConnectionProvider { - - static override createProxy(container: interfaces.Container, path: string, arg?: object): JsonRpcProxy { - return container.get(ElectronIpcConnectionProvider).createProxy(path, arg); - } - - constructor() { - super(); - ipcRenderer.on(THEIA_ELECTRON_IPC_CHANNEL_NAME, (event: ElectronEvent, data: string) => { - this.handleIncomingRawMessage(data); - }); - } - - protected createChannel(id: number): WebSocketChannel { - return new WebSocketChannel(id, content => { - ipcRenderer.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, content); - }); - } - -} diff --git a/packages/core/src/electron-browser/messaging/electron-ipc-renderer-connection.ts b/packages/core/src/electron-browser/messaging/electron-ipc-renderer-connection.ts new file mode 100644 index 0000000000000..c0c6c45708b51 --- /dev/null +++ b/packages/core/src/electron-browser/messaging/electron-ipc-renderer-connection.ts @@ -0,0 +1,60 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { Event as ElectronEvent, IpcRenderer } from '@theia/core/electron-shared/electron'; +import { injectable } from 'inversify'; +import { AbstractConnection, Connection, ConnectionState } from '../../common'; +import { pushDisposableListener } from '../../common/node-event-utils'; + +/** + * @internal + */ +@injectable() +export class IpcRendererConnection extends AbstractConnection { + + state = ConnectionState.OPENING; + + protected channel?: string; + protected ipcRenderer?: IpcRenderer; + + /** + * @param channel The Electron IPC channel to use for sending messages over this connection. + * @param ipcRenderer The Electron `IpcRenderer` API. + */ + initialize(channel: string, ipcRenderer: IpcRenderer): Connection { + this.channel = channel; + this.ipcRenderer = ipcRenderer; + pushDisposableListener(this.disposables, this.ipcRenderer, this.channel, (event: ElectronEvent, message: any) => { + this.ensureState(ConnectionState.OPENED); + this.onMessageEmitter.fire(message); + }); + this.setOpenedAndEmit(); + return this; + } + + sendMessage(message: any): void { + this.ensureState(ConnectionState.OPENED); + this.ipcRenderer!.send(this.channel!, message); + } + + close(): void { + this.setClosedAndEmit(); + this.dispose(); + this.ipcRenderer = undefined; + } +} diff --git a/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts b/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts index 52d9e05a2e882..b027f8d38c552 100644 --- a/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts +++ b/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts @@ -15,14 +15,66 @@ // ***************************************************************************** import { ContainerModule } from 'inversify'; +import { ipcRenderer } from '../../../electron-shared/electron'; import { FrontendApplicationContribution } from '../../browser/frontend-application'; import { WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider'; +import { bindServiceProvider, ConnectionMultiplexer, DeferredConnectionFactory, JsonRpcConnectionFactory, ProxyProvider, RpcProxying, ServiceProvider } from '../../common'; +import { waitForRemote } from '../../common/connection'; +import { DefaultConnectionMultiplexer } from '../../common/connection-multiplexer'; +import { DefaultRpcProxyProvider } from '../../common/rpc'; +import { ElectronMainAndFrontend } from '../../electron-common'; +import { IpcRendererConnection } from './electron-ipc-renderer-connection'; import { ElectronWebSocketConnectionProvider } from './electron-ws-connection-provider'; -import { ElectronIpcConnectionProvider } from './electron-ipc-connection-provider'; export const messagingFrontendModule = new ContainerModule(bind => { + // #region transients + bind(IpcRendererConnection).toSelf().inTransientScope(); + // #endregion + // #region singletons bind(ElectronWebSocketConnectionProvider).toSelf().inSingletonScope(); + // #endregion + // #region services bind(FrontendApplicationContribution).toService(ElectronWebSocketConnectionProvider); bind(WebSocketConnectionProvider).toService(ElectronWebSocketConnectionProvider); - bind(ElectronIpcConnectionProvider).toSelf().inSingletonScope(); + // #endregion + // #region ElectronMainAndFrontend + bindServiceProvider(bind, ElectronMainAndFrontend); + bind(ConnectionMultiplexer) + .toDynamicValue(ctx => { + const deferredConnectionFactory = ctx.container.get(DeferredConnectionFactory); + const ipcConnection = ctx.container.get(IpcRendererConnection).initialize(ElectronMainAndFrontend, ipcRenderer); + const deferredConnection = deferredConnectionFactory(waitForRemote(ipcConnection)); + return ctx.container.get(DefaultConnectionMultiplexer).initialize(deferredConnection); + }) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndFrontend); + bind(ProxyProvider) + .toDynamicValue(ctx => { + const multiplexer = ctx.container.getNamed(ConnectionMultiplexer, ElectronMainAndFrontend); + const jsonRpcConnectionFactory = ctx.container.get(JsonRpcConnectionFactory); + return ctx.container.get(DefaultRpcProxyProvider).initialize( + (serviceId, serviceParams) => jsonRpcConnectionFactory(multiplexer.open({ serviceId, serviceParams })) + ); + }) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndFrontend); + bind(FrontendApplicationContribution) + .toDynamicValue(ctx => ({ + initialize(): void { + const multiplexer = ctx.container.getNamed(ConnectionMultiplexer, ElectronMainAndFrontend); + const serviceProvider = ctx.container.getNamed(ServiceProvider, ElectronMainAndFrontend); + const jsonRpcConnectionFactory = ctx.container.get(JsonRpcConnectionFactory); + const rpcProxying = ctx.container.get(RpcProxying); + multiplexer.listen(({ serviceId, serviceParams }, accept, next) => { + const service = serviceProvider.getService(serviceId, serviceParams); + if (service) { + rpcProxying.serve(service, jsonRpcConnectionFactory(accept())); + } else { + next(); + } + }); + } + })) + .inSingletonScope(); + // #endregion }); diff --git a/packages/core/src/electron-browser/window/electron-window-module.ts b/packages/core/src/electron-browser/window/electron-window-module.ts index 0318d184aabcd..2f9184f41fdd5 100644 --- a/packages/core/src/electron-browser/window/electron-window-module.ts +++ b/packages/core/src/electron-browser/window/electron-window-module.ts @@ -21,15 +21,16 @@ import { FrontendApplicationContribution } from '../../browser/frontend-applicat import { ElectronClipboardService } from '../electron-clipboard-service'; import { ClipboardService } from '../../browser/clipboard-service'; import { ElectronMainWindowService, electronMainWindowServicePath } from '../../electron-common/electron-main-window-service'; -import { ElectronIpcConnectionProvider } from '../messaging/electron-ipc-connection-provider'; import { bindWindowPreferences } from './electron-window-preferences'; import { FrontendApplicationStateService } from '../../browser/frontend-application-state'; import { ElectronFrontendApplicationStateService } from './electron-frontend-application-state'; +import { ProxyProvider } from '../../common'; +import { ElectronMainAndFrontend } from '../../electron-common'; export default new ContainerModule((bind, unbind, isBound, rebind) => { - bind(ElectronMainWindowService).toDynamicValue(context => - ElectronIpcConnectionProvider.createProxy(context.container, electronMainWindowServicePath) - ).inSingletonScope(); + bind(ElectronMainWindowService) + .toDynamicValue(ctx => ctx.container.getNamed(ProxyProvider, ElectronMainAndFrontend).getProxy(electronMainWindowServicePath)) + .inSingletonScope(); bindWindowPreferences(bind); bind(WindowService).to(ElectronWindowService).inSingletonScope(); bind(FrontendApplicationContribution).toService(WindowService); diff --git a/packages/core/src/electron-common/electron-main-window-service.ts b/packages/core/src/electron-common/electron-main-window-service.ts index 310e4be2f6080..385932f530702 100644 --- a/packages/core/src/electron-common/electron-main-window-service.ts +++ b/packages/core/src/electron-common/electron-main-window-service.ts @@ -14,11 +14,12 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +import { MaybePromise, serviceIdentifier, servicePath } from '../common'; import { NewWindowOptions } from '../common/window'; -export const electronMainWindowServicePath = '/services/electron-window'; -export const ElectronMainWindowService = Symbol('ElectronMainWindowService'); +export const electronMainWindowServicePath = servicePath('/services/electron-window'); +export const ElectronMainWindowService = serviceIdentifier('ElectronMainWindowService'); export interface ElectronMainWindowService { - openNewWindow(url: string, options?: NewWindowOptions): undefined; - openNewDefaultWindow(): void; + openNewWindow(url: string, options?: NewWindowOptions): MaybePromise; + openNewDefaultWindow(): MaybePromise; } diff --git a/packages/core/src/electron-common/index.ts b/packages/core/src/electron-common/index.ts new file mode 100644 index 0000000000000..baaeb23c59286 --- /dev/null +++ b/packages/core/src/electron-common/index.ts @@ -0,0 +1,17 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +export { ElectronMainAndBackend, ElectronMainAndFrontend } from './messaging/electron-messages'; diff --git a/packages/core/src/electron-common/messaging/electron-messages.ts b/packages/core/src/electron-common/messaging/electron-messages.ts index 9c3d15c1beca2..8d32177a8a577 100644 --- a/packages/core/src/electron-common/messaging/electron-messages.ts +++ b/packages/core/src/electron-common/messaging/electron-messages.ts @@ -51,3 +51,18 @@ export interface CloseRequestArguments { cancelChannel: string; reason: StopReason; } + +/** + * Name of the channel used with `ipcMain.on/emit`. + */ +export const THEIA_ELECTRON_IPC_CHANNEL_NAME: string = 'theia-electron-ipc'; + +/** + * Represent bindings between the `electron-main` runtime and the `electron-node` (backend) runtime. + */ +export const ElectronMainAndBackend: string = 'ElectronMainAndBackend'; + +/** + * Represent bindings between the `electron-main` runtime and the `electron-browser` (frontend) runtime. + */ +export const ElectronMainAndFrontend: string = 'ElectronMainAndFrontend'; diff --git a/packages/core/src/electron-main/electron-main-application-module.ts b/packages/core/src/electron-main/electron-main-application-module.ts index f2954d8533c49..8444dda187094 100644 --- a/packages/core/src/electron-main/electron-main-application-module.ts +++ b/packages/core/src/electron-main/electron-main-application-module.ts @@ -15,48 +15,232 @@ // ***************************************************************************** import { ContainerModule } from 'inversify'; +import { THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../electron-common/messaging/electron-messages'; import { v4 } from 'uuid'; +import { + AnyConnection, + bindServiceProvider, + ConnectionMultiplexer, + ConnectionTransformer, + ContainerScopeReady, + DeferredConnectionFactory, + DisposableCollection, + JsonRpcConnectionFactory, + ProxyProvider, + RpcProxying, + ServiceContribution, + ServiceProvider +} from '../common'; +import { DefaultConnectionMultiplexer } from '../common/connection-multiplexer'; +import { ContainerScope, ContainerScopeFactory } from '../common/container-scope'; import { bindContributionProvider } from '../common/contribution-provider'; -import { JsonRpcConnectionHandler } from '../common/messaging/proxy-factory'; -import { ElectronSecurityToken } from '../electron-common/electron-token'; +import { getAllNamedOptional } from '../common/inversify-utils'; +import { DefaultRpcProxyProvider } from '../common/rpc'; +import { ElectronMainAndBackend, ElectronMainAndFrontend } from '../electron-common'; import { ElectronMainWindowService, electronMainWindowServicePath } from '../electron-common/electron-main-window-service'; +import { ElectronSecurityToken } from '../electron-common/electron-token'; +import { cluster } from '../node'; +import { InProcessProxyProvider } from '../node/in-process-proxy-provider'; +import { NodeIpcConnectionFactory } from '../node/messaging/ipc-connection'; import { ElectronMainApplication, ElectronMainApplicationContribution, ElectronMainProcessArgv } from './electron-main-application'; import { ElectronMainWindowServiceImpl } from './electron-main-window-service-impl'; -import { ElectronMessagingContribution } from './messaging/electron-messaging-contribution'; -import { ElectronMessagingService } from './messaging/electron-messaging-service'; -import { ElectronConnectionHandler } from '../electron-common/messaging/electron-connection-handler'; import { ElectronSecurityTokenService } from './electron-security-token-service'; +import { WebContentsConnection } from './electron-web-contents-connection'; import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory, WindowApplicationConfig } from './theia-electron-window'; +import { waitForRemote } from '../common/connection'; +import { pushDisposableListener } from '../common/node-event-utils'; const electronSecurityToken: ElectronSecurityToken = { value: v4() }; // eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any)[ElectronSecurityToken] = electronSecurityToken; +/** + * Main container module loaded when a `BrowserWindow` running the Theia + * frontend is opened. + */ +export const ElectronMainAndFrontendContainerModule = new ContainerModule(bind => { + bindServiceProvider(bind, ElectronMainAndFrontend); + bind(ContainerScopeReady) + .toFunction(container => { + const multiplexer = container.getNamed(ConnectionMultiplexer, ElectronMainAndFrontend); + const serviceProvider = container.getNamed(ServiceProvider, ElectronMainAndFrontend); + const jsonRpcConnectionFactory = container.get(JsonRpcConnectionFactory); + const rpcProxying = container.get(RpcProxying); + multiplexer.listen(({ serviceId, serviceParams }, accept, next) => { + const service = serviceProvider.getService(serviceId, serviceParams); + if (service) { + rpcProxying.serve(service, jsonRpcConnectionFactory(accept())); + } else { + next(); + } + }); + }) + .whenTargetNamed(ElectronMainAndFrontend); + bind(ConnectionMultiplexer) + .toDynamicValue(ctx => { + const theiaWindow = ctx.container.get(TheiaElectronWindow); + const deferredConnectionFactory = ctx.container.get(DeferredConnectionFactory); + const { webContents } = theiaWindow.window; + const ipcConnection = ctx.container.get(WebContentsConnection).initialize(ElectronMainAndFrontend, webContents, { + closeOnNavigation: true, + targetFrameId: webContents.mainFrame.routingId + }); + const deferredConnection = deferredConnectionFactory(waitForRemote(ipcConnection)); + return ctx.container.get(DefaultConnectionMultiplexer).initialize(deferredConnection); + }) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndFrontend); + bind(ProxyProvider) + .toDynamicValue(ctx => { + const multiplexer = ctx.container.getNamed(ConnectionMultiplexer, ElectronMainAndFrontend); + const jsonRpcConnectionProvider = ctx.container.get(JsonRpcConnectionFactory); + return ctx.container.get(DefaultRpcProxyProvider).initialize( + serviceId => jsonRpcConnectionProvider(multiplexer.open({ serviceId })) + ); + }) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndFrontend); + bind(ServiceContribution) + .toDynamicValue(ctx => ({ + [electronMainWindowServicePath]: () => ctx.container.get(ElectronMainWindowService) + })) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndFrontend); +}); + export default new ContainerModule(bind => { - bind(ElectronMainApplication).toSelf().inSingletonScope(); - bind(ElectronMessagingContribution).toSelf().inSingletonScope(); + // #region constants bind(ElectronSecurityToken).toConstantValue(electronSecurityToken); - bind(ElectronSecurityTokenService).toSelf().inSingletonScope(); - - bindContributionProvider(bind, ElectronConnectionHandler); - bindContributionProvider(bind, ElectronMessagingService.Contribution); - bindContributionProvider(bind, ElectronMainApplicationContribution); - - bind(ElectronMainApplicationContribution).toService(ElectronMessagingContribution); - - bind(ElectronMainWindowService).to(ElectronMainWindowServiceImpl).inSingletonScope(); - bind(ElectronConnectionHandler).toDynamicValue(context => - new JsonRpcConnectionHandler(electronMainWindowServicePath, - () => context.container.get(ElectronMainWindowService)) - ).inSingletonScope(); - - bind(ElectronMainProcessArgv).toSelf().inSingletonScope(); - - bind(TheiaElectronWindow).toSelf(); + // #endregion + // #region transients + bind(WebContentsConnection).toSelf().inTransientScope(); + bind(TheiaElectronWindow).toSelf().inTransientScope(); + // #endregion + // #region factories bind(TheiaElectronWindowFactory).toFactory(({ container }) => (options, config) => { const child = container.createChild(); child.bind(TheiaBrowserWindowOptions).toConstantValue(options); child.bind(WindowApplicationConfig).toConstantValue(config); return child.get(TheiaElectronWindow); }); + // #endregion + // #region contribution providers + bindContributionProvider(bind, ElectronMainApplicationContribution); + // #endregion + // #region singletons + bind(ElectronMainProcessArgv).toSelf().inSingletonScope(); + bind(ElectronMainApplication).toSelf().inSingletonScope(); + bind(ElectronSecurityTokenService).toSelf().inSingletonScope(); + bind(ElectronMainWindowService).to(ElectronMainWindowServiceImpl).inSingletonScope(); + // #endregion + // #region ElectronMainAndFrontend + bind(ElectronMainApplicationContribution) + .toDynamicValue(ctx => ({ + onStart(app): void { + const containerScopeFactory = ctx.container.get(ContainerScopeFactory); + app.onDidCreateTheiaWindow(theiaWindow => { + recursiveCreateFrontendContainerScope(theiaWindow); + }); + function recursiveCreateFrontendContainerScope(theiaWindow: TheiaElectronWindow): void { + const disposables = new DisposableCollection(); + disposables.push(createFrontendContainerScope(theiaWindow)); + pushDisposableListener(disposables, theiaWindow.window.webContents, 'did-navigate', function (): void { + disposables.dispose(); + recursiveCreateFrontendContainerScope(theiaWindow); + }); + theiaWindow.onDidClose(() => disposables.dispose(), undefined, disposables); + } + function createFrontendContainerScope(theiaWindow: TheiaElectronWindow): ContainerScope { + const child = ctx.container.createChild(); + child.bind(TheiaElectronWindow).toConstantValue(theiaWindow); + const modules = ctx.container.getAllNamed(ContainerModule, ElectronMainAndFrontend); + child.load(...modules); + const callbacks = getAllNamedOptional(child, ContainerScopeReady, ElectronMainAndFrontend); + return containerScopeFactory(child, callbacks); + } + } + })) + .inSingletonScope(); + bind(ContainerModule) + .toConstantValue(ElectronMainAndFrontendContainerModule) + .whenTargetNamed(ElectronMainAndFrontend); + // #endregion + // #region ElectronMainAndBackend + bindServiceProvider(bind, ElectronMainAndBackend); + if (cluster) { + // We need to setup the JSON-RPC connection between electron-main and backend: + // We'll multiplex messages over a Node IPC connection and talk JSON-RPC over the channels. + bind(ConnectionMultiplexer) + .toDynamicValue(ctx => { + const app = ctx.container.get(ElectronMainApplication); + const transformer = ctx.container.get(ConnectionTransformer); + const nodeIpcConnectionFactory = ctx.container.get(NodeIpcConnectionFactory); + const deferredConnectionFactory = ctx.container.get(DeferredConnectionFactory); + const deferredConnection = deferredConnectionFactory(app.backendProcess.then(backend => { + const backendIpc = nodeIpcConnectionFactory(backend); + const sharedIpc: AnyConnection = transformer(backendIpc, { + decode: (message, emit) => { + if (typeof message === 'object' && THEIA_ELECTRON_IPC_CHANNEL_NAME in message) { + emit(message[THEIA_ELECTRON_IPC_CHANNEL_NAME]); + } + }, + encode: (message, write) => { + write({ [THEIA_ELECTRON_IPC_CHANNEL_NAME]: message }); + } + }); + return waitForRemote(sharedIpc); + })); + return ctx.container.get(DefaultConnectionMultiplexer).initialize(deferredConnection); + }) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndBackend); + bind(ElectronMainApplicationContribution) + .toDynamicValue(ctx => ({ + onStart(): void { + const multiplexer = ctx.container.getNamed(ConnectionMultiplexer, ElectronMainAndBackend); + const serviceProvider = ctx.container.getNamed(ServiceProvider, ElectronMainAndBackend); + const jsonRpcConnectionFactory = ctx.container.get(JsonRpcConnectionFactory); + const rpcProxying = ctx.container.get(RpcProxying); + multiplexer.listen(({ serviceId, serviceParams }, accept, next) => { + const service = serviceProvider.getService(serviceId, serviceParams); + if (service) { + rpcProxying.serve(service, jsonRpcConnectionFactory(accept())); + } else { + next(); + } + }); + } + })) + .inSingletonScope(); + bind(ProxyProvider) + .toDynamicValue(ctx => { + const multiplexer = ctx.container.getNamed(ConnectionMultiplexer, ElectronMainAndBackend); + const jsonRpcConnectionFactory = ctx.container.get(JsonRpcConnectionFactory); + return ctx.container.get(DefaultRpcProxyProvider).initialize( + (serviceId, serviceParams) => jsonRpcConnectionFactory(multiplexer.open({ serviceId, serviceParams })) + ); + }) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndBackend); + } else { + const inProcessProxyProvider = Symbol(); + bind(inProcessProxyProvider) + .toDynamicValue(ctx => { + const serviceProvider = ctx.container.getNamed(ServiceProvider, ElectronMainAndBackend); + return ctx.container.get(InProcessProxyProvider).initialize(ElectronMainAndBackend, serviceProvider); + }) + .inSingletonScope(); + bind(ProxyProvider) + .toDynamicValue(ctx => ctx.container.get(inProcessProxyProvider)) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndBackend); + bind(ElectronMainApplicationContribution) + .toDynamicValue(ctx => ({ + onStart(): void { + ctx.container.get(inProcessProxyProvider); // start listening + } + })) + .inSingletonScope(); + } + // #endregion }); diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index 3902a91f626f5..9c38ce2015d35 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -21,17 +21,16 @@ import * as path from 'path'; import { Argv } from 'yargs'; import { AddressInfo } from 'net'; import { promises as fs } from 'fs'; -import { fork, ForkOptions } from 'child_process'; +import { ChildProcess, fork, ForkOptions } from 'child_process'; import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; import URI from '../common/uri'; import { FileUri } from '../node/file-uri'; import { Deferred } from '../common/promise-util'; -import { MaybePromise } from '../common/types'; import { ContributionProvider } from '../common/contribution-provider'; import { ElectronSecurityTokenService } from './electron-security-token-service'; import { ElectronSecurityToken } from '../electron-common/electron-token'; import Storage = require('electron-store'); -import { Disposable, DisposableCollection, isOSX, isWindows } from '../common'; +import { Disposable, DisposableCollection, Emitter, Event, isOSX, isWindows, MaybePromise, serviceIdentifier } from '../common'; import { RequestTitleBarStyle, Restart, StopReason, @@ -41,9 +40,10 @@ import { import { DEFAULT_WINDOW_HASH } from '../common/window'; import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory } from './theia-electron-window'; import { ElectronMainApplicationGlobals } from './electron-main-constants'; -import { createDisposableListener } from './event-utils'; +import { pushDisposableListener } from '../common/node-event-utils'; export { ElectronMainApplicationGlobals }; +import { cluster } from '../node'; const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs'); @@ -91,7 +91,7 @@ export interface ElectronMainExecutionParams { * ElectronIpcConnectionProvider.createProxy(context.container, electronMainWindowServicePath) * ).inSingletonScope(); */ -export const ElectronMainApplicationContribution = Symbol('ElectronMainApplicationContribution'); +export const ElectronMainApplicationContribution = serviceIdentifier('ElectronMainApplicationContribution'); export interface ElectronMainApplicationContribution { /** * The application is ready and is starting. This is the time to initialize @@ -180,15 +180,24 @@ export class ElectronMainApplication { windowstate?: TheiaBrowserWindowOptions }>(); - protected readonly _backendPort = new Deferred(); - readonly backendPort = this._backendPort.promise; + protected _backendPort = new Deferred(); + protected _backendProcess = new Deferred(); + protected onDidCreateTheiaWindowEmitter = new Emitter(); protected _config: FrontendApplicationConfig | undefined; protected useNativeWindowFrame: boolean = true; protected didUseNativeWindowFrameOnStart = new Map(); protected windows = new Map(); protected restarting = false; + get backendProcess(): Promise { + return this._backendProcess.promise; + } + + get backendPort(): Promise { + return this._backendPort.promise; + } + get config(): FrontendApplicationConfig { if (!this._config) { throw new Error('You have to start the application first.'); @@ -196,6 +205,13 @@ export class ElectronMainApplication { return this._config; } + /** + * Emitted once the Theia window has finished loading. + */ + get onDidCreateTheiaWindow(): Event { + return this.onDidCreateTheiaWindowEmitter.event; + } + async start(config: FrontendApplicationConfig): Promise { this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native'; this._config = config; @@ -252,6 +268,9 @@ export class ElectronMainApplication { electronWindow.onDidClose(() => this.windows.delete(id)); this.attachSaveWindowState(electronWindow.window); electronRemoteMain.enable(electronWindow.window.webContents); + electronWindow.window.webContents.once('did-finish-load', () => { + this.onDidCreateTheiaWindowEmitter.fire(electronWindow); + }); return electronWindow.window; } @@ -277,7 +296,6 @@ export class ElectronMainApplication { } options.x = options.x! + 30; options.y = options.y! + 30; - } } return options; @@ -381,11 +399,11 @@ export class ElectronMainApplication { } delayedSaveTimeout = setTimeout(() => this.saveWindowState(electronWindow), 1000); }; - createDisposableListener(electronWindow, 'close', () => { + pushDisposableListener(windowStateListeners, electronWindow, 'close', () => { this.saveWindowState(electronWindow); - }, windowStateListeners); - createDisposableListener(electronWindow, 'resize', saveWindowStateDelayed, windowStateListeners); - createDisposableListener(electronWindow, 'move', saveWindowStateDelayed, windowStateListeners); + }); + pushDisposableListener(windowStateListeners, electronWindow, 'resize', saveWindowStateDelayed); + pushDisposableListener(windowStateListeners, electronWindow, 'move', saveWindowStateDelayed); windowStateListeners.push(Disposable.create(() => { try { this.didUseNativeWindowFrameOnStart.delete(electronWindow.id); } catch { } })); this.didUseNativeWindowFrameOnStart.set(electronWindow.id, this.useNativeWindowFrame); electronWindow.once('closed', () => windowStateListeners.dispose()); @@ -418,9 +436,10 @@ export class ElectronMainApplication { * Return a string unique to the current display layout. */ protected getCurrentScreenLayout(): string { - return screen.getAllDisplays().map( - display => `${display.bounds.x}:${display.bounds.y}:${display.bounds.width}:${display.bounds.height}` - ).sort().join('-'); + return screen.getAllDisplays() + .map(display => `${display.bounds.x}:${display.bounds.y}:${display.bounds.width}:${display.bounds.height}`) + .sort() + .join('-'); } /** @@ -429,8 +448,6 @@ export class ElectronMainApplication { * @return Running server's port promise. */ protected async startBackend(): Promise { - // Check if we should run everything as one process. - const noBackendFork = process.argv.indexOf('--no-cluster') !== -1; // We cannot use the `process.cwd()` as the application project path (the location of the `package.json` in other words) // in a bundled electron application because it depends on the way we start it. For instance, on OS X, these are a differences: // https://github.com/eclipse-theia/theia/issues/3297#issuecomment-439172274 @@ -438,26 +455,40 @@ export class ElectronMainApplication { // Set the electron version for both the dev and the production mode. (https://github.com/eclipse-theia/theia/issues/3254) // Otherwise, the forked backend processes will not know that they're serving the electron frontend. process.env.THEIA_ELECTRON_VERSION = process.versions.electron; - if (noBackendFork) { + if (!cluster) { + this._backendProcess.promise.catch(() => { /* prevent `uncaughtRejection` error */ }); + this._backendProcess.reject(new Error('running the backend in the current process')); process.env[ElectronSecurityToken] = JSON.stringify(this.electronSecurityToken); // The backend server main file is supposed to export a promise resolving with the port used by the http(s) server. - const address: AddressInfo = await require(this.globals.THEIA_BACKEND_MAIN_PATH); - return address.port; + const { port }: AddressInfo = await require(this.globals.THEIA_BACKEND_MAIN_PATH); + return port; } else { const backendProcess = fork( this.globals.THEIA_BACKEND_MAIN_PATH, this.processArgv.getProcessArgvWithoutBin(), await this.getForkOptions(), ); + this._backendProcess.resolve(backendProcess); return new Promise((resolve, reject) => { - // The backend server main file is also supposed to send the resolved http(s) server port via IPC. - backendProcess.on('message', (address: AddressInfo) => { - resolve(address.port); - }); - backendProcess.on('error', error => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function messageListener(message: any): void { + if ('port' in message && 'address' in message) { + resolve(message.port); + cleanup(); + } + }; + function errorListener(error: unknown): void { reject(error); - }); - app.on('quit', () => { + cleanup(); + }; + function cleanup(): void { + backendProcess.off('message', messageListener); + backendProcess.off('error', errorListener); + }; + // The backend server main file is also supposed to send the resolved http(s) server port via IPC. + backendProcess.on('message', messageListener); + backendProcess.once('error', errorListener); + app.once('quit', () => { // Only issue a kill signal if the backend process is running. // eslint-disable-next-line no-null/no-null if (backendProcess.exitCode === null && backendProcess.signalCode === null) { diff --git a/packages/core/src/electron-main/electron-web-contents-connection.ts b/packages/core/src/electron-main/electron-web-contents-connection.ts new file mode 100644 index 0000000000000..af2f6e137d1a5 --- /dev/null +++ b/packages/core/src/electron-main/electron-web-contents-connection.ts @@ -0,0 +1,141 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { IpcMainEvent, WebContents } from '@theia/core/electron-shared/electron'; +import { injectable } from 'inversify'; +import { AbstractConnection, Connection, ConnectionState } from '../common'; +import { pushDisposableListener } from '../common/node-event-utils'; + +/** + * @internal + */ +export interface WebContentsConnectionOptions { + /** + * Close the connection when the frame referrenced by {@link targetFrameId} + * navigates. + * + * @default false + */ + closeOnNavigation?: boolean + /** + * Listen to async and/or sync IPC message types. + * + * @default 'both' + */ + ipcMessageKind?: 'async' | 'sync' | 'both' + /** + * The frame to listen/talk to. + * + * If not specified messages will be sent to the main frame exclusively + * and messages from any frame will be emitted back. + */ + targetFrameId?: number +} + +/** + * @internal + */ +export type DidFrameNavigateParams = [ + event: unknown, + url: string, + httpResponseCode: number, + httpStatusText: string, + isMainFrame: boolean, + frameProcessId: number, + frameRoutingId: number +]; + +/** + * @internal + * + * Represents a `Connection` over one channel of Electron's IPC API. + */ +@injectable() +export class WebContentsConnection extends AbstractConnection { + + state = ConnectionState.OPENING; + + protected channel?: string; + protected webContents?: WebContents; + protected targetFrameId?: number; + protected closeOnNavigation?: boolean; + + initialize(channel: string, webContents: WebContents, options: WebContentsConnectionOptions = {}): Connection { + this.channel = channel; + this.webContents = webContents; + this.targetFrameId = options.targetFrameId; + this.closeOnNavigation = options.closeOnNavigation ?? false; + this.listenForIpcMessages(options.ipcMessageKind ?? 'both'); + pushDisposableListener(this.disposables, this.webContents, 'destroyed', () => this.close()); + if (this.closeOnNavigation) { + pushDisposableListener( + this.disposables, + this.webContents, + 'did-frame-navigate', + (event, url, httpResponseCode, httpStatusText, isMainFrame, frameProcessId, frameRoutingId) => { + if (this.isTargetFrame(frameRoutingId)) { + this.close(); + } + } + ); + } + this.setOpenedAndEmit(); + return this; + } + + sendMessage(message: any): void { + this.ensureState(ConnectionState.OPENED); + if (this.targetFrameId === undefined) { + this.webContents!.send(this.channel!, message); + } else { + this.webContents!.sendToFrame(this.targetFrameId, this.channel!, message); + } + } + + close(): void { + this.setClosedAndEmit(); + this.dispose(); + this.webContents = undefined; + } + + protected listenForIpcMessages(ipcMessageKind: 'async' | 'sync' | 'both'): void { + const ipcMessageListener = this.handleIpcMessage.bind(this); + if (ipcMessageKind === 'both') { + pushDisposableListener(this.disposables, this.webContents!, 'ipc-message', ipcMessageListener); + pushDisposableListener(this.disposables, this.webContents!, 'ipc-message-sync', ipcMessageListener); + } else if (ipcMessageKind === 'async') { + pushDisposableListener(this.disposables, this.webContents!, 'ipc-message', ipcMessageListener); + } else if (ipcMessageKind === 'sync') { + pushDisposableListener(this.disposables, this.webContents!, 'ipc-message-sync', ipcMessageListener); + } + } + + /** + * Note that contrary to what the documentation says, the event type for `ipc-message(-sync)` is `IpcMainEvent`. + */ + protected handleIpcMessage(event: IpcMainEvent, incomingChannel: string, message: any): void { + this.ensureState(ConnectionState.OPENED); + if (this.isTargetFrame(event.senderFrame.routingId) && incomingChannel === this.channel!) { + this.onMessageEmitter.fire(message); + } + } + + protected isTargetFrame(frameId: number): boolean { + return this.targetFrameId === undefined || this.targetFrameId === frameId; + } +} diff --git a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts deleted file mode 100644 index 071796c5cf0ca..0000000000000 --- a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts +++ /dev/null @@ -1,131 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2020 Ericsson and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0. -// -// This Source Code may also be made available under the following Secondary -// Licenses when the conditions for such availability set forth in the Eclipse -// Public License v. 2.0 are satisfied: GNU General Public License, version 2 -// with the GNU Classpath Exception which is available at -// https://www.gnu.org/software/classpath/license.html. -// -// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 -// ***************************************************************************** - -import { IpcMainEvent, ipcMain, WebContents } from '@theia/electron/shared/electron'; -import { inject, injectable, named, postConstruct } from 'inversify'; -import { MessageConnection } from 'vscode-ws-jsonrpc'; -import { createWebSocketConnection } from 'vscode-ws-jsonrpc/lib/socket/connection'; -import { ContributionProvider } from '../../common/contribution-provider'; -import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; -import { MessagingContribution } from '../../node/messaging/messaging-contribution'; -import { ConsoleLogger } from '../../node/messaging/logger'; -import { ElectronConnectionHandler, THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../../electron-common/messaging/electron-connection-handler'; -import { ElectronMainApplicationContribution } from '../electron-main-application'; -import { ElectronMessagingService } from './electron-messaging-service'; - -/** - * This component replicates the role filled by `MessagingContribution` but for Electron. - * Unlike the WebSocket based implementation, we do not expect to receive - * connection events. Instead, we'll create channels based on incoming `open` - * events on the `ipcMain` channel. - * - * This component allows communication between renderer process (frontend) and electron main process. - */ -@injectable() -export class ElectronMessagingContribution implements ElectronMainApplicationContribution, ElectronMessagingService { - - @inject(ContributionProvider) @named(ElectronMessagingService.Contribution) - protected readonly messagingContributions: ContributionProvider; - - @inject(ContributionProvider) @named(ElectronConnectionHandler) - protected readonly connectionHandlers: ContributionProvider; - - protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers(); - protected readonly windowChannels = new Map>(); - - @postConstruct() - protected init(): void { - ipcMain.on(THEIA_ELECTRON_IPC_CHANNEL_NAME, (event: IpcMainEvent, data: string) => { - this.handleIpcMessage(event, data); - }); - } - - onStart(): void { - for (const contribution of this.messagingContributions.getContributions()) { - contribution.configure(this); - } - for (const connectionHandler of this.connectionHandlers.getContributions()) { - this.channelHandlers.push(connectionHandler.path, (params, channel) => { - const connection = createWebSocketConnection(channel, new ConsoleLogger()); - connectionHandler.onConnection(connection); - }); - } - } - - listen(spec: string, callback: (params: ElectronMessagingService.PathParams, connection: MessageConnection) => void): void { - this.ipcChannel(spec, (params, channel) => { - const connection = createWebSocketConnection(channel, new ConsoleLogger()); - callback(params, connection); - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ipcChannel(spec: string, callback: (params: any, channel: WebSocketChannel) => void): void { - this.channelHandlers.push(spec, callback); - } - - protected handleIpcMessage(event: IpcMainEvent, data: string): void { - const sender = event.sender; - try { - // Get the channel map for a given window id - let channels = this.windowChannels.get(sender.id)!; - if (!channels) { - this.windowChannels.set(sender.id, channels = new Map()); - } - // Start parsing the message to extract the channel id and route - const message: WebSocketChannel.Message = JSON.parse(data.toString()); - // Someone wants to open a logical channel - if (message.kind === 'open') { - const { id, path } = message; - const channel = this.createChannel(id, sender); - if (this.channelHandlers.route(path, channel)) { - channel.ready(); - channels.set(id, channel); - channel.onClose(() => channels.delete(id)); - } else { - console.error('Cannot find a service for the path: ' + path); - } - } else { - const { id } = message; - const channel = channels.get(id); - if (channel) { - channel.handleMessage(message); - } else { - console.error('The ipc channel does not exist', id); - } - } - const close = () => { - for (const channel of Array.from(channels.values())) { - channel.close(undefined, 'webContent destroyed'); - } - channels.clear(); - }; - sender.once('did-navigate', close); // When refreshing the browser window. - sender.once('destroyed', close); // When closing the browser window. - } catch (error) { - console.error('IPC: Failed to handle message', { error, data }); - } - } - - protected createChannel(id: number, sender: WebContents): WebSocketChannel { - return new WebSocketChannel(id, content => { - if (!sender.isDestroyed()) { - sender.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, content); - } - }); - } - -} diff --git a/packages/core/src/electron-main/messaging/electron-messaging-service.ts b/packages/core/src/electron-main/messaging/electron-messaging-service.ts deleted file mode 100644 index dde3fdde1d181..0000000000000 --- a/packages/core/src/electron-main/messaging/electron-messaging-service.ts +++ /dev/null @@ -1,40 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2020 Ericsson and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0. -// -// This Source Code may also be made available under the following Secondary -// Licenses when the conditions for such availability set forth in the Eclipse -// Public License v. 2.0 are satisfied: GNU General Public License, version 2 -// with the GNU Classpath Exception which is available at -// https://www.gnu.org/software/classpath/license.html. -// -// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 -// ***************************************************************************** - -import type { MessageConnection } from 'vscode-jsonrpc'; -import type { WebSocketChannel } from '../../common/messaging/web-socket-channel'; - -export interface ElectronMessagingService { - /** - * Accept a JSON-RPC connection on the given path. - * A path supports the route syntax: https://github.com/rcs/route-parser#what-can-i-use-in-my-routes. - */ - listen(path: string, callback: (params: ElectronMessagingService.PathParams, connection: MessageConnection) => void): void; - /** - * Accept an ipc channel on the given path. - * A path supports the route syntax: https://github.com/rcs/route-parser#what-can-i-use-in-my-routes. - */ - ipcChannel(path: string, callback: (params: ElectronMessagingService.PathParams, socket: WebSocketChannel) => void): void; -} -export namespace ElectronMessagingService { - export interface PathParams { - [name: string]: string - } - export const Contribution = Symbol('ElectronMessagingService.Contribution'); - export interface Contribution { - configure(service: ElectronMessagingService): void; - } -} diff --git a/packages/core/src/electron-main/theia-electron-window.ts b/packages/core/src/electron-main/theia-electron-window.ts index 8494ac75d720b..046d2f9e682bc 100644 --- a/packages/core/src/electron-main/theia-electron-window.ts +++ b/packages/core/src/electron-main/theia-electron-window.ts @@ -20,8 +20,8 @@ import { APPLICATION_STATE_CHANGE_SIGNAL, CLOSE_REQUESTED_SIGNAL, RELOAD_REQUEST import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain, IpcMainEvent } from '../../electron-shared/electron'; import { inject, injectable, postConstruct } from '../../shared/inversify'; import { ElectronMainApplicationGlobals } from './electron-main-constants'; -import { DisposableCollection, Emitter, Event, isWindows } from '../common'; -import { createDisposableListener } from './event-utils'; +import { DisposableCollection, Emitter, Event, isWindows, serviceIdentifier } from '../common'; +import { pushDisposableListener } from '../common/node-event-utils'; /** * Theia tracks the maximized state of Electron Browser Windows. @@ -42,20 +42,27 @@ export const TheiaBrowserWindowOptions = Symbol('TheiaBrowserWindowOptions'); export const WindowApplicationConfig = Symbol('WindowApplicationConfig'); export type WindowApplicationConfig = FrontendApplicationConfig; +/** + * A `TheiaElectronWindow` is assumed to host a Theia frontend in its main + * frame. + * + * Note that while only one Theia frontend instance lives in the + * {@link TheiaElectronWindow} main frame at a time, the frontend may reload + * itself, creating a whole new frontend instance. + */ @injectable() export class TheiaElectronWindow { @inject(TheiaBrowserWindowOptions) protected readonly options: TheiaBrowserWindowOptions; @inject(WindowApplicationConfig) protected readonly config: WindowApplicationConfig; @inject(ElectronMainApplicationGlobals) protected readonly globals: ElectronMainApplicationGlobals; - protected onDidCloseEmitter = new Emitter(); + protected disposables = new DisposableCollection(); + protected onDidCloseEmitter = this.disposables.pushThru(new Emitter()); get onDidClose(): Event { return this.onDidCloseEmitter.event; } - protected readonly toDispose = new DisposableCollection(this.onDidCloseEmitter); - protected _window: BrowserWindow; get window(): BrowserWindow { return this._window; @@ -83,18 +90,18 @@ export class TheiaElectronWindow { } protected attachCloseListeners(): void { - createDisposableListener(this._window, 'closed', () => { + pushDisposableListener(this.disposables, this._window, 'closed', () => { this.onDidCloseEmitter.fire(); this.dispose(); - }, this.toDispose); - createDisposableListener(this._window, 'close', async event => { + }); + pushDisposableListener(this.disposables, this._window, 'close', (event: Electron.Event) => { // User has already indicated that it is OK to close this window, or the window is being closed before it's ready. if (this.closeIsConfirmed || this.applicationState !== 'ready') { return; } event.preventDefault(); this.handleStopRequest(() => this.doCloseWindow(), StopReason.Close); - }, this.toDispose); + }); } protected doCloseWindow(): void { @@ -140,17 +147,19 @@ export class TheiaElectronWindow { const temporaryDisposables = new DisposableCollection(); return new Promise(resolve => { this._window.webContents.send(CLOSE_REQUESTED_SIGNAL, { confirmChannel, cancelChannel, reason }); - createDisposableListener(ipcMain, confirmChannel, (e: IpcMainEvent) => { + pushDisposableListener(temporaryDisposables, ipcMain, confirmChannel, (e: IpcMainEvent) => { if (this.isSender(e)) { resolve(true); } - }, temporaryDisposables); - createDisposableListener(ipcMain, cancelChannel, (e: IpcMainEvent) => { + }); + pushDisposableListener(temporaryDisposables, ipcMain, cancelChannel, (e: IpcMainEvent) => { if (this.isSender(e)) { resolve(false); } - }, temporaryDisposables); - }).finally(() => temporaryDisposables.dispose()); + }); + }).finally( + () => temporaryDisposables.dispose() + ); } protected restoreMaximizedState(): void { @@ -162,19 +171,19 @@ export class TheiaElectronWindow { } protected trackApplicationState(): void { - createDisposableListener(ipcMain, APPLICATION_STATE_CHANGE_SIGNAL, (e: IpcMainEvent, state: FrontendApplicationState) => { + pushDisposableListener(this.disposables, ipcMain, APPLICATION_STATE_CHANGE_SIGNAL, (e: IpcMainEvent, state: FrontendApplicationState) => { if (this.isSender(e)) { this.applicationState = state; } - }, this.toDispose); + }); } protected attachReloadListener(): void { - createDisposableListener(ipcMain, RELOAD_REQUESTED_SIGNAL, (e: IpcMainEvent) => { + pushDisposableListener(this.disposables, ipcMain, RELOAD_REQUESTED_SIGNAL, (e: IpcMainEvent) => { if (this.isSender(e)) { this.reload(); } - }, this.toDispose); + }); } protected isSender(e: IpcMainEvent): boolean { @@ -182,12 +191,11 @@ export class TheiaElectronWindow { } dispose(): void { - this.toDispose.dispose(); + this.disposables.dispose(); } } +export const TheiaElectronWindowFactory = serviceIdentifier('TheiaElectronWindowFactory'); export interface TheiaElectronWindowFactory { (options: TheiaBrowserWindowOptions, config: FrontendApplicationConfig): TheiaElectronWindow; } - -export const TheiaElectronWindowFactory = Symbol('TheiaElectronWindowFactory'); diff --git a/packages/core/src/electron-node/electron-node-backend-module.ts b/packages/core/src/electron-node/electron-node-backend-module.ts new file mode 100644 index 0000000000000..9267fab074f7f --- /dev/null +++ b/packages/core/src/electron-node/electron-node-backend-module.ts @@ -0,0 +1,114 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from 'inversify'; +import { + AnyConnection, + bindServiceProvider, + ConnectionMultiplexer, + ConnectionTransformer, + DeferredConnectionFactory, + JsonRpcConnectionFactory, + ProxyProvider, + RpcProxying, + ServiceProvider +} from '../common'; +import { waitForRemote } from '../common/connection'; +import { DefaultConnectionMultiplexer } from '../common/connection-multiplexer'; +import { DefaultRpcProxyProvider } from '../common/rpc'; +import { ElectronMainAndBackend } from '../electron-common'; +import { THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../electron-common/messaging/electron-messages'; +import { BackendApplicationContribution, cluster } from '../node'; +import { InProcessProxyProvider } from '../node/in-process-proxy-provider'; +import { NodeIpcConnectionFactory } from '../node/messaging/ipc-connection'; + +export default new ContainerModule(bind => { + // #region ElectronMainAndBackend + bindServiceProvider(bind, ElectronMainAndBackend); + if (cluster) { + // We need to setup the JSON-RPC connection between electron-main and backend: + // We'll multiplex messages over a Node IPC connection and talk JSON-RPC over the channels. + bind(ConnectionMultiplexer) + .toDynamicValue(ctx => { + const transformer = ctx.container.get(ConnectionTransformer); + const nodeIpcConnectionFactory = ctx.container.get(NodeIpcConnectionFactory); + const deferredConnectionFactory = ctx.container.get(DeferredConnectionFactory); + const parentIpc = nodeIpcConnectionFactory(process); + const sharedIpc: AnyConnection = transformer(parentIpc, { + decode: (message, emit) => { + if (typeof message === 'object' && THEIA_ELECTRON_IPC_CHANNEL_NAME in message) { + emit(message[THEIA_ELECTRON_IPC_CHANNEL_NAME]); + } + }, + encode: (message, write) => { + write({ [THEIA_ELECTRON_IPC_CHANNEL_NAME]: message }); + } + }); + const deferredConnection = deferredConnectionFactory(waitForRemote(sharedIpc)); + return ctx.container.get(DefaultConnectionMultiplexer).initialize(deferredConnection); + }) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndBackend); + bind(BackendApplicationContribution) + .toDynamicValue(ctx => ({ + initialize(): void { + const multiplexer = ctx.container.getNamed(ConnectionMultiplexer, ElectronMainAndBackend); + const serviceProvider = ctx.container.getNamed(ServiceProvider, ElectronMainAndBackend); + const jsonRpcConnectionFactory = ctx.container.get(JsonRpcConnectionFactory); + const rpcProxying = ctx.container.get(RpcProxying); + multiplexer.listen(({ serviceId, serviceParams }, accept, next) => { + const service = serviceProvider.getService(serviceId, serviceParams); + if (service) { + rpcProxying.serve(service, jsonRpcConnectionFactory(accept())); + } else { + next(); + } + }); + } + })) + .inSingletonScope(); + bind(ProxyProvider) + .toDynamicValue(ctx => { + const multiplexer = ctx.container.getNamed(ConnectionMultiplexer, ElectronMainAndBackend); + const jsonRpcConnectionFactory = ctx.container.get(JsonRpcConnectionFactory); + return ctx.container.get(DefaultRpcProxyProvider).initialize( + (serviceId, serviceParams) => jsonRpcConnectionFactory(multiplexer.open({ serviceId, serviceParams })) + ); + }) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndBackend); + } else { + const inProcessProxyProvider = Symbol(); + bind(inProcessProxyProvider) + .toDynamicValue(ctx => { + const serviceProvider = ctx.container.getNamed(ServiceProvider, ElectronMainAndBackend); + return ctx.container.get(InProcessProxyProvider).initialize(ElectronMainAndBackend, serviceProvider); + }) + .inSingletonScope(); + bind(ProxyProvider) + .toDynamicValue(ctx => ctx.container.get(inProcessProxyProvider)) + .inSingletonScope() + .whenTargetNamed(ElectronMainAndBackend); + bind(BackendApplicationContribution) + .toDynamicValue(ctx => ({ + initialize(): void { + ctx.container.get(inProcessProxyProvider); + } + })) + .inSingletonScope(); + } + // #endregion +}); diff --git a/packages/core/src/electron-node/token/electron-token-validator.ts b/packages/core/src/electron-node/token/electron-token-validator.ts index 6b58d4b009472..3752b65137c27 100644 --- a/packages/core/src/electron-node/token/electron-token-validator.ts +++ b/packages/core/src/electron-node/token/electron-token-validator.ts @@ -18,7 +18,6 @@ import * as http from 'http'; import * as cookie from 'cookie'; import * as crypto from 'crypto'; import { injectable, postConstruct } from 'inversify'; -import { MaybePromise } from '../../common'; import { ElectronSecurityToken } from '../../electron-common/electron-token'; import { WsRequestValidatorContribution } from '../../node/ws-request-validators'; @@ -35,7 +34,7 @@ export class ElectronTokenValidator implements WsRequestValidatorContribution { this.electronSecurityToken = this.getToken(); } - allowWsUpgrade(request: http.IncomingMessage): MaybePromise { + async allowWsUpgrade(request: http.IncomingMessage): Promise { return this.allowRequest(request); } diff --git a/packages/core/src/node/backend-application.ts b/packages/core/src/node/backend-application.ts index 8926a0afd307f..d4a4942ef597e 100644 --- a/packages/core/src/node/backend-application.ts +++ b/packages/core/src/node/backend-application.ts @@ -21,7 +21,7 @@ import * as express from 'express'; import * as yargs from 'yargs'; import * as fs from 'fs-extra'; import { inject, named, injectable, postConstruct } from 'inversify'; -import { ContributionProvider, MaybePromise, Stopwatch } from '../common'; +import { ContributionProvider, MaybePromise, serviceIdentifier, Stopwatch } from '../common'; import { CliContribution } from './cli'; import { Deferred } from '../common/promise-util'; import { environment } from '../common/index'; @@ -45,7 +45,7 @@ export const BackendApplicationServer = Symbol('BackendApplicationServer'); */ export interface BackendApplicationServer extends BackendApplicationContribution { } -export const BackendApplicationContribution = Symbol('BackendApplicationContribution'); +export const BackendApplicationContribution = serviceIdentifier('BackendApplicationContribution'); /** * Contribution for hooking into the backend lifecycle. */ diff --git a/packages/core/src/node/cluster.ts b/packages/core/src/node/cluster.ts new file mode 100644 index 0000000000000..459483f98e722 --- /dev/null +++ b/packages/core/src/node/cluster.ts @@ -0,0 +1,17 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +export const cluster = !process.argv.includes('--no-cluster'); diff --git a/packages/core/src/node/in-process-proxy-provider.spec.ts b/packages/core/src/node/in-process-proxy-provider.spec.ts new file mode 100644 index 0000000000000..b4d2d5db7cd71 --- /dev/null +++ b/packages/core/src/node/in-process-proxy-provider.spec.ts @@ -0,0 +1,35 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { AssertionError } from 'assert'; +import { expect } from 'chai'; +import { InProcessExchanger } from './in-process-proxy-provider'; + +describe('InProcessExchanger', () => { + + it('should exchange data between two callers', async () => { + const exchanger = new InProcessExchanger(); + const firstPromise = exchanger.exchange(1); + const secondPromise = exchanger.exchange(2); + const [first, second] = await Promise.all([firstPromise, secondPromise]); + expect(first).eq(2); + expect(second).eq(1); + await exchanger.exchange(3).then( + ok => { throw new AssertionError({ message: 'a third call to .exchange() should have failed' }); }, + error => { /* pass the test */ } + ); + }); +}); diff --git a/packages/core/src/node/in-process-proxy-provider.ts b/packages/core/src/node/in-process-proxy-provider.ts new file mode 100644 index 0000000000000..d61814216bdeb --- /dev/null +++ b/packages/core/src/node/in-process-proxy-provider.ts @@ -0,0 +1,84 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { inject, injectable } from 'inversify'; +import { Deferred, LazyProxyFactory, ProxyProvider, ServiceProvider } from '../common'; + +@injectable() +export class InProcessProxyProvider implements ProxyProvider { + + protected remoteServiceProviderPromise: Promise; + + @inject(LazyProxyFactory) + protected lazyProxyFactory: LazyProxyFactory; + + initialize(namespace: string | symbol, serviceProvider: ServiceProvider): this { + this.remoteServiceProviderPromise = this.getExchanger(namespace).exchange(serviceProvider); + return this; + } + + getProxy(serviceId: string): any { + // getProxy will most likely get called before all code locations were able to exchange, + // hence the need for a lazy proxy to defer potential requests until everyone initialized. + return this.lazyProxyFactory(this.remoteServiceProviderPromise.then( + remoteServiceProvider => remoteServiceProvider.getService(serviceId) + )); + } + + protected getExchanger(namespace: string | symbol): Exchanger { + return (globalThis as any)[this.getGlobalKey(namespace)] ??= new InProcessExchanger(); + } + + protected getGlobalKey(namespace: string | symbol): string | symbol { + if (typeof namespace === 'symbol') { + return namespace; + } + // This will resolve to the same symbol when running in the same process: + return Symbol.for(`theia#in-process-proxy-provider#${namespace}`); + } +} + +/** + * This mechanism is analog to a barrier-lock where two parties can exchange some data before being released. + */ +export interface Exchanger { + exchange(data: T): Promise; +} + +export class InProcessExchanger implements Exchanger { + + protected done = false; + protected waiting?: { first: T, second: Deferred }; + + async exchange(instance: T): Promise { + if (this.done) { + throw new Error('the exchange already happened!'); + } else if (!this.waiting) { + const first = instance; + const second = new Deferred(); + this.waiting = { first, second }; + return second.promise; + } else { + const { first, second } = this.waiting; + second.resolve(instance); + this.done = true; + this.waiting = undefined; + return first; + } + } +} diff --git a/packages/core/src/node/index.ts b/packages/core/src/node/index.ts index 00a095f757f9a..c0b463ff3a0ba 100644 --- a/packages/core/src/node/index.ts +++ b/packages/core/src/node/index.ts @@ -19,3 +19,5 @@ export * from './debug'; export * from './file-uri'; export * from './messaging'; export * from './cli'; + +export { cluster } from './cluster'; diff --git a/packages/core/src/node/messaging/ipc-connection.ts b/packages/core/src/node/messaging/ipc-connection.ts new file mode 100644 index 0000000000000..92de85f241538 --- /dev/null +++ b/packages/core/src/node/messaging/ipc-connection.ts @@ -0,0 +1,73 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { injectable } from 'inversify'; +import { pushDisposableListener } from '../../common/node-event-utils'; +import { AbstractConnection, Connection, ConnectionState, serviceIdentifier } from '../../common'; + +export const NodeIpcConnectionFactory = serviceIdentifier('NodeIpcConnectionFactory'); +export type NodeIpcConnectionFactory = (proc: ProcessLike) => Connection; + +export interface ProcessLike extends NodeJS.EventEmitter { + disconnect(): void + on(event: 'message', listener: (message: any) => void): this + on(event: 'disconnect', listener: () => void): this + send?(message: any): void +} + +@injectable() +export class NodeIpcConnection extends AbstractConnection { + + state = ConnectionState.OPENING; + + protected proc?: ProcessLike; + + initialize(proc: ProcessLike): this { + this.proc = proc; + if (typeof this.proc.send !== 'function') { + this.dispose(); + throw new Error('cannot communicate with process through Node\'s IPC'); + } + pushDisposableListener(this.disposables, this.proc, 'message', message => { + this.onMessageEmitter.fire(message); + }); + pushDisposableListener(this.disposables, this.proc, 'disconnect', () => { + if (this.state !== ConnectionState.CLOSED) { + this.dispose(); + } + }); + this.setOpenedAndEmit(); + return this; + } + + sendMessage(message: string | object): void { + this.ensureState(ConnectionState.OPENED); + this.proc!.send!(message); + } + + close(): void { + this.dispose(); + this.proc!.disconnect(); + this.proc = undefined; + } + + override dispose(): void { + this.setClosedAndEmit(); + super.dispose(); + } +} diff --git a/packages/core/src/node/node-common-module.ts b/packages/core/src/node/node-common-module.ts new file mode 100644 index 0000000000000..12edb7930242e --- /dev/null +++ b/packages/core/src/node/node-common-module.ts @@ -0,0 +1,29 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from 'inversify'; +import { InProcessProxyProvider } from './in-process-proxy-provider'; +import { NodeIpcConnection, NodeIpcConnectionFactory } from './messaging/ipc-connection'; + +export default new ContainerModule(bind => { + // #region singletons + bind(InProcessProxyProvider).toSelf().inSingletonScope(); + // #endregion + // #region factories + bind(NodeIpcConnectionFactory) + .toFunction(proc => new NodeIpcConnection().initialize(proc)); + // #endregion +}); diff --git a/packages/editor/src/browser/quick-editor-service.ts b/packages/editor/src/browser/quick-editor-service.ts index 5dea426286e1e..f3293b987fb87 100644 --- a/packages/editor/src/browser/quick-editor-service.ts +++ b/packages/editor/src/browser/quick-editor-service.ts @@ -21,7 +21,8 @@ import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { OpenerService } from '@theia/core/lib/browser/opener-service'; import { QuickAccessProvider, QuickAccessRegistry, QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access'; import { filterItems, QuickPickItem, QuickPickSeparator } from '@theia/core/lib/browser/quick-input/quick-input-service'; -import { EditorManager, EditorWidget } from '.'; +import { EditorManager } from './editor-manager'; +import { EditorWidget } from './editor-widget'; @injectable() export class QuickEditorService implements QuickAccessContribution, QuickAccessProvider { diff --git a/packages/filesystem/src/common/filesystem-utils.spec.ts b/packages/filesystem/src/common/filesystem-utils.spec.ts index 231b1cdb90bd6..06d50707dedbf 100644 --- a/packages/filesystem/src/common/filesystem-utils.spec.ts +++ b/packages/filesystem/src/common/filesystem-utils.spec.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { FileSystemUtils } from '.'; +import { FileSystemUtils } from './filesystem-utils'; import { FileStat } from './files'; import { expect } from 'chai'; diff --git a/packages/scm-extra/src/browser/history/index.ts b/packages/scm-extra/src/browser/history/index.ts index 7c180aeea830f..5021b6899d2cd 100644 --- a/packages/scm-extra/src/browser/history/index.ts +++ b/packages/scm-extra/src/browser/history/index.ts @@ -14,16 +14,5 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { ScmProvider } from '@theia/scm/lib/browser/scm-provider'; -import { ScmHistorySupport } from './scm-history-widget'; - -export { ScmHistorySupport }; - -export interface ScmHistoryProvider extends ScmProvider { - historySupport?: ScmHistorySupport; -} -export namespace ScmHistoryProvider { - export function is(scmProvider: ScmProvider | undefined): scmProvider is ScmHistoryProvider { - return !!scmProvider && 'historySupport' in scmProvider; - } -} +export { ScmHistorySupport } from './scm-history-widget'; +export { ScmHistoryProvider } from './scm-history-provider'; diff --git a/packages/scm-extra/src/browser/history/scm-history-provider.ts b/packages/scm-extra/src/browser/history/scm-history-provider.ts new file mode 100644 index 0000000000000..d3c88499e3ab2 --- /dev/null +++ b/packages/scm-extra/src/browser/history/scm-history-provider.ts @@ -0,0 +1,27 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ScmProvider } from '@theia/scm/lib/browser/scm-provider'; +import { ScmHistorySupport } from './scm-history-widget'; + +export interface ScmHistoryProvider extends ScmProvider { + historySupport?: ScmHistorySupport; +} +export namespace ScmHistoryProvider { + export function is(scmProvider: ScmProvider | undefined): scmProvider is ScmHistoryProvider { + return !!scmProvider && 'historySupport' in scmProvider; + } +} diff --git a/packages/scm-extra/src/browser/history/scm-history-widget.tsx b/packages/scm-extra/src/browser/history/scm-history-widget.tsx index e6147f44db17a..5cc608f0e1128 100644 --- a/packages/scm-extra/src/browser/history/scm-history-widget.tsx +++ b/packages/scm-extra/src/browser/history/scm-history-widget.tsx @@ -21,7 +21,7 @@ import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { Message } from '@theia/core/shared/@phosphor/messaging'; import { AutoSizer, List, ListRowRenderer, ListRowProps, InfiniteLoader, IndexRange, ScrollParams, CellMeasurerCache, CellMeasurer } from '@theia/core/shared/react-virtualized'; import URI from '@theia/core/lib/common/uri'; -import { ScmHistoryProvider } from '.'; +import { ScmHistoryProvider } from './scm-history-provider'; import { SCM_HISTORY_ID, SCM_HISTORY_MAX_COUNT, SCM_HISTORY_LABEL } from './scm-history-contribution'; import { ScmHistoryCommit, ScmFileChange, ScmFileChangeNode } from '../scm-file-change-node'; import { ScmAvatarService } from '@theia/scm/lib/browser/scm-avatar-service'; diff --git a/packages/workspace/src/browser/workspace-trust-service.ts b/packages/workspace/src/browser/workspace-trust-service.ts index 9c513fe6186b3..5ea88f0b9243a 100644 --- a/packages/workspace/src/browser/workspace-trust-service.ts +++ b/packages/workspace/src/browser/workspace-trust-service.ts @@ -21,7 +21,7 @@ import { nls } from '@theia/core/lib/common/nls'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { WorkspaceService } from '.'; +import { WorkspaceService } from './workspace-service'; import { WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WorkspaceTrustPrompt } from './workspace-trust-preferences'; diff --git a/yarn.lock b/yarn.lock index 0ea2565c595e8..7f05ef4c9fc8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.1.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" - integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== +"@ampproject/remapping@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.0.tgz#72becdf17ee44b2d1ac5651fb12f1952c336fe23" + integrity sha512-d5RysTlJ7hmw5Tw4UxgxcY3lkMe92n8sXCcuLPAyIAHK6j8DefDwtGnVVDgOnv+RnEosulDJ9NPKQL27bDId0g== dependencies: "@jridgewell/trace-mapping" "^0.3.0" @@ -23,25 +23,30 @@ dependencies: "@babel/highlight" "^7.16.7" -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.7": +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.8": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.0.tgz#86850b8597ea6962089770952075dcaabb8dba34" + integrity sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng== + +"@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ== "@babel/core@^7.10.0", "@babel/core@^7.14.8", "@babel/core@^7.7.5": - version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.8.tgz#3dac27c190ebc3a4381110d46c80e77efe172e1a" - integrity sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ== + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.2.tgz#2c77fc430e95139d816d39b113b31bf40fb22337" + integrity sha512-R3VH5G42VSDolRHyUO4V2cfag8WHcZyxdq5Z/m8Xyb92lW/Erm/6kM+XtRFGf3Mulre3mveni2NHfEUws8wSvw== dependencies: - "@ampproject/remapping" "^2.1.0" + "@ampproject/remapping" "^2.0.0" "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.7" - "@babel/helper-compilation-targets" "^7.17.7" - "@babel/helper-module-transforms" "^7.17.7" - "@babel/helpers" "^7.17.8" - "@babel/parser" "^7.17.8" + "@babel/generator" "^7.17.0" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.17.2" + "@babel/parser" "^7.17.0" "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.3" + "@babel/traverse" "^7.17.0" "@babel/types" "^7.17.0" convert-source-map "^1.7.0" debug "^4.1.0" @@ -49,7 +54,16 @@ json5 "^2.1.2" semver "^6.3.0" -"@babel/generator@^7.17.3", "@babel/generator@^7.17.7": +"@babel/generator@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.0.tgz#7bd890ba706cd86d3e2f727322346ffdbf98f65e" + integrity sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw== + dependencies: + "@babel/types" "^7.17.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/generator@^7.17.3": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.7.tgz#8da2599beb4a86194a3b24df6c085931d9ee45ad" integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w== @@ -73,7 +87,7 @@ "@babel/helper-explode-assignable-expression" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.7": +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== @@ -83,7 +97,20 @@ browserslist "^4.17.5" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.6": +"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7": + version "7.17.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.1.tgz#9699f14a88833a7e055ce57dcd3ffdcd25186b21" + integrity sha512-JBdSr/LtyYIno/pNnJ75lBcqc3Z1XXujzPanHqjvvrhOA+DTceTFuJi8XjmWTZh4r3fsdfqaCMN0iZemdkxZHQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + +"@babel/helper-create-class-features-plugin@^7.17.6": version "7.17.6" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz#3778c1ed09a7f3e65e6d6e0f6fbfcc53809d92c9" integrity sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg== @@ -215,7 +242,7 @@ "@babel/traverse" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/helper-simple-access@^7.17.7": +"@babel/helper-simple-access@^7.16.7", "@babel/helper-simple-access@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== @@ -256,13 +283,13 @@ "@babel/traverse" "^7.16.8" "@babel/types" "^7.16.8" -"@babel/helpers@^7.17.8": - version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.8.tgz#288450be8c6ac7e4e44df37bcc53d345e07bc106" - integrity sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw== +"@babel/helpers@^7.17.2": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.2.tgz#23f0a0746c8e287773ccd27c14be428891f63417" + integrity sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ== dependencies: "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.3" + "@babel/traverse" "^7.17.0" "@babel/types" "^7.17.0" "@babel/highlight@^7.10.4", "@babel/highlight@^7.16.7": @@ -274,7 +301,12 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.8": +"@babel/parser@^7.16.7", "@babel/parser@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c" + integrity sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw== + +"@babel/parser@^7.17.3": version "7.17.8" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ== @@ -649,13 +681,13 @@ babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-commonjs@^7.14.5", "@babel/plugin-transform-modules-commonjs@^7.16.8": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz#d86b217c8e45bb5f2dbc11eefc8eab62cf980d19" - integrity sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA== + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz#cdee19aae887b16b9d331009aa9a219af7c86afe" + integrity sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA== dependencies: - "@babel/helper-module-transforms" "^7.17.7" + "@babel/helper-module-transforms" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-simple-access" "^7.16.7" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-systemjs@^7.16.7": @@ -900,9 +932,9 @@ "@babel/plugin-transform-typescript" "^7.16.7" "@babel/runtime@^7.10.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": - version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" - integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" + integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== dependencies: regenerator-runtime "^0.13.4" @@ -915,7 +947,23 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3": +"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.0.tgz#3143e5066796408ccc880a33ecd3184f3e75cd30" + integrity sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.0" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.0" + "@babel/types" "^7.17.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.17.3": version "7.17.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== @@ -961,9 +1009,9 @@ global-tunnel-ng "^2.7.1" "@electron/remote@^2.0.1 <2.0.4 || >2.0.4": - version "2.0.8" - resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-2.0.8.tgz#85ff321f0490222993207106e2f720273bb1a5c3" - integrity sha512-P10v3+iFCIvEPeYzTWWGwwHmqWnjoh8RYnbtZAb3RlQefy4guagzIwcWtfftABIfm6JJTNQf4WPSKWZOpLmHXw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-2.0.1.tgz#810cbc595a21f0f94641eb2d7e8264063a3f84de" + integrity sha512-bGX4/yB2bPZwXm1DsxgoABgH0Cz7oFtXJgkerB8VrStYdTyvhGAULzNLRn9rVmeAuC3VUDXaXpZIlZAZHpsLIA== "@eslint/eslintrc@^0.4.3": version "0.4.3" @@ -1032,19 +1080,19 @@ chalk "^4.0.0" "@jridgewell/resolve-uri@^3.0.3": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" - integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz#b876e3feefb9c8d3aa84014da28b5e52a0640d72" + integrity sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg== "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.11" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" - integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== + version "1.4.10" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.10.tgz#baf57b4e2a690d4f38560171f91783656b7f8186" + integrity sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg== "@jridgewell/trace-mapping@^0.3.0": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" - integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.2.tgz#e051581782a770c30ba219634f2019241c5d3cde" + integrity sha512-9KzzH4kMjA2XmBRHfqG2/Vtl7s92l6uNDd0wW7frDE+EUvQFGqNXhWp0UGJjSkt3v2AYjzOZn1QO9XaTNJIt1Q== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -2084,9 +2132,9 @@ integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== "@sindresorhus/is@^4.0.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" - integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + version "4.4.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.4.0.tgz#e277e5bdbdf7cb1e20d320f02f5e2ed113cd3185" + integrity sha512-QppPM/8l3Mawvh4rn9CNEYIU9bxpXUCRMaX9yUpvBk1nMKusLKpfXGDEKExKaPhLzcn3lzil7pR6rnJ11HgeRQ== "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3": version "1.8.3" @@ -2096,9 +2144,9 @@ type-detect "4.0.8" "@sinonjs/fake-timers@>=5": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.1.tgz#7b698e0b9d12d93611f06ee143c30ced848e2840" - integrity sha512-Wp5vwlZ0lOqpSYGKqr53INws9HLkt6JDc/pDZcPf7bchQnrXJMXPns8CXx0hFikMSGSWfvtvvpb2gtMVfkWagA== + version "9.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.0.tgz#8c92c56f195e0bed4c893ba59c8e3d55831ca0df" + integrity sha512-M8vapsv9qQupMdzrVzkn5rb9jG7aUTEPAZdMtME2PuBaefksFZVE2C1g4LBRTkF/k3nRDNbDc5tp5NFC1PEYxA== dependencies: "@sinonjs/commons" "^1.7.0" @@ -2255,7 +2303,7 @@ resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-0.0.20.tgz#cae698714dd61ebee5ab3f2aeb9a34ba1011735a" integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw== -"@types/eslint-scope@^3.7.3": +"@types/eslint-scope@^3.7.0": version "3.7.3" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g== @@ -2271,11 +2319,16 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^0.0.51": +"@types/estree@*": version "0.0.51" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@^0.0.50": + version "0.0.50" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== + "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -2374,9 +2427,9 @@ integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= "@types/keyv@*": - version "3.1.4" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" - integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.3.tgz#1c9aae32872ec1f20dcdaee89a9f3ba88f465e41" + integrity sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg== dependencies: "@types/node" "*" @@ -2487,9 +2540,9 @@ form-data "^3.0.0" "@types/node@*", "@types/node@12", "@types/node@>=10.0.0", "@types/node@^10.14.22", "@types/node@^14.6.2": - version "12.20.47" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.47.tgz#ca9237d51f2a2557419688511dab1c8daf475188" - integrity sha512-BzcaRsnFuznzOItW1WpQrDHM7plAa7GIDMZ6b5pnMbkqEtM/6WCOhvZar39oeMQP79gwvFUWjjptE7/KGcNqFg== + version "12.20.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.43.tgz#6cf47894da4a4748c62fccf720ba269e1b1ff5a4" + integrity sha512-HCfJdaYqJX3BCzeihgZrD7b85Cu05OC/GVJ4kEYIflwUs4jbnUlLLWoq7hw1LBcdvUyehO+gr6P5JQ895/2ZfA== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -2548,26 +2601,26 @@ "@types/react" "^16" "@types/react-virtualized@^9.18.3": - version "9.21.20" - resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.20.tgz#756c78b5512a2a1804fdaf749a5f5cff3d805e5b" - integrity sha512-i8nZf1LpuX5rG4DZLaPGayIQwjxsZwmst5VdNhEznDTENel9p3A735AdRRp2iueFOyOuWBmaEaDxg8AD3GHilA== + version "9.21.17" + resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.17.tgz#3c30104d2d38832b748419b58ac04f839ee838fe" + integrity sha512-8JWc9GzurY9L0iqlL+dPziUsyP7MsY27m8k8mCEZNvuYwA3o2vvbkRkb1IoCdV2XYwPU4w8wfaBHungLGr0b4w== dependencies: "@types/prop-types" "*" "@types/react" "*" "@types/react@*": - version "17.0.43" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55" - integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A== + version "17.0.39" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.39.tgz#d0f4cde092502a6db00a1cded6e6bf2abb7633ce" + integrity sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" csstype "^3.0.2" "@types/react@^16", "@types/react@^16.8.0": - version "16.14.24" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.24.tgz#f2c5e9fa78f83f769884b83defcf7924b9eb5c82" - integrity sha512-e7U2WC8XQP/xfR7bwhOhNFZKPTfW1ph+MiqtudKb8tSV8RyCsovQx2sNVtKoOryjxFKpHPPC/yNiGfdeVM5Gyw== + version "16.14.23" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.23.tgz#37201b9f2324c5ff8fa4600dbf19079dfdffc880" + integrity sha512-WngBZLuSkP4IAgPi0HOsGCHo6dn3CcuLQnCfC17VbA7YBgipZiZoTOhObwl/93DsFW0Y2a/ZXeonpW4DxirEJg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2654,9 +2707,9 @@ "@types/sinonjs__fake-timers" "*" "@types/sinonjs__fake-timers@*": - version "8.1.2" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e" - integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA== + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" + integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== "@types/stack-utils@^2.0.0": version "2.0.1" @@ -3152,9 +3205,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.3: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.3, ajv@^8.8.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" - integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + version "8.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.10.0.tgz#e573f719bd3af069017e3b66538ab968d040e54d" + integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -3278,10 +3331,10 @@ archy@^1.0.0: resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= -are-we-there-yet@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz#ba20bd6b553e31d62fc8c31bd23d22b95734390d" - integrity sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw== +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== dependencies: delegates "^1.0.0" readable-stream "^3.6.0" @@ -3661,13 +3714,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -4113,7 +4159,7 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.3: +color-support@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== @@ -4395,9 +4441,9 @@ copy-webpack-plugin@^8.1.1: serialize-javascript "^5.0.1" core-js-compat@^3.20.2, core-js-compat@^3.21.0: - version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82" - integrity sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g== + version "3.21.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.0.tgz#bcc86aa5a589cee358e7a7fa0a4979d5a76c3885" + integrity sha512-OSXseNPSK2OPJa6GdtkMz/XxeXx8/CJvfhQWTqd6neuUraujcL4jVsjkLQz1OWnax8xVQJnRPe0V2jqNWORA+A== dependencies: browserslist "^4.19.1" semver "7.0.0" @@ -4473,12 +4519,12 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: which "^2.0.1" css-loader@^6.2.0: - version "6.7.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.1.tgz#e98106f154f6e1baf3fc3bc455cb9981c1d5fd2e" - integrity sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw== + version "6.6.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.6.0.tgz#c792ad5510bd1712618b49381bd0310574fafbd3" + integrity sha512-FK7H2lisOixPT406s5gZM1S3l8GrfhEBT3ZiL2UX1Ng1XWs0y2GPllz/OTyvbaHe12VgQrIXIzuEGVlbUhodqg== dependencies: icss-utils "^5.1.0" - postcss "^8.4.7" + postcss "^8.4.5" postcss-modules-extract-imports "^3.0.0" postcss-modules-local-by-default "^4.0.0" postcss-modules-scope "^3.0.0" @@ -4561,9 +4607,9 @@ debug@3.2.6: ms "^2.1.1" debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@~4.3.1, debug@~4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" @@ -4864,9 +4910,9 @@ domexception@^1.0.1: webidl-conversions "^4.0.2" dompurify@^2.2.9: - version "2.3.6" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.6.tgz#2e019d7d7617aacac07cbbe3d88ae3ad354cf875" - integrity sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg== + version "2.3.5" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.5.tgz#c83ed5a3ae5ce23e52efe654ea052ffb358dd7e3" + integrity sha512-kD+f8qEaa42+mjdOpKeztu9Mfx5bv9gVLO6K9jRx4uGvh6Wv06Srn4jr1wPNY2OOUGGSKHNFN+A8MA3v0E0QAQ== dot-prop@^5.1.0: version "5.3.0" @@ -4998,9 +5044,9 @@ electron-window@^0.8.0: is-electron-renderer "^2.0.0" electron@^15.3.5: - version "15.4.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-15.4.2.tgz#16bef2c1ed2f5f637218b0e906c20c93a484c4bb" - integrity sha512-g5JOsr4RkvLNbk3vcyFZMcc7vlM1A6+wYJCIk9QF5jjkIHktVA0I1PnMZ15MQ9HJsrzvXC8iREekjR1bEw6UAA== + version "15.3.6" + resolved "https://registry.yarnpkg.com/electron/-/electron-15.3.6.tgz#19b9aee1e063b1983b3d7f535567d90e0e1b4d04" + integrity sha512-mOOTcZH/Vlq9GP3B8G3aMvZQ4eZyCjUZZpLccABqsPyLUMEixcdx750DQ7M+iHYyo0BWxj/JuHblzQXgNnPkfg== dependencies: "@electron/get" "^1.13.0" "@types/node" "^14.6.2" @@ -5055,7 +5101,7 @@ engine.io-client@~6.1.1: xmlhttprequest-ssl "~2.0.0" yeast "0.1.2" -engine.io-parser@~5.0.0, engine.io-parser@~5.0.3: +engine.io-parser@~5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.3.tgz#ca1f0d7b11e290b4bfda251803baea765ed89c09" integrity sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg== @@ -5063,9 +5109,9 @@ engine.io-parser@~5.0.0, engine.io-parser@~5.0.3: "@socket.io/base64-arraybuffer" "~1.0.2" engine.io@~6.1.0: - version "6.1.3" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.1.3.tgz#f156293d011d99a3df5691ac29d63737c3302e6f" - integrity sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA== + version "6.1.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.1.2.tgz#e7b9d546d90c62246ffcba4d88594be980d3855a" + integrity sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -5075,10 +5121,10 @@ engine.io@~6.1.0: cookie "~0.4.1" cors "~2.8.5" debug "~4.3.1" - engine.io-parser "~5.0.3" + engine.io-parser "~5.0.0" ws "~8.2.3" -enhanced-resolve@^5.9.2: +enhanced-resolve@^5.8.3: version "5.9.2" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9" integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA== @@ -5782,9 +5828,9 @@ flatted@^3.1.0: integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== follow-redirects@^1.14.0: - version "1.14.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== font-awesome@^4.7.0: version "4.7.0" @@ -5857,9 +5903,9 @@ fs-constants@^1.0.0: integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== fs-extra@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" - integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -5943,18 +5989,19 @@ fuzzy@^0.1.3: integrity sha1-THbsL/CsGjap3M+aAN+GIweNTtg= gauge@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.3.tgz#286cf105c1962c659f0963058fb05116c1b82d3f" - integrity sha512-ICw1DhAwMtb22rYFwEHgJcx1JCwJGv3x6G0OQUq56Nge+H4Q8JEwr8iveS0XFlsUNSI67F5ffMGK25bK4Pmskw== + version "4.0.0" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.0.tgz#afba07aa0374a93c6219603b1fb83eaa2264d8f8" + integrity sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw== dependencies: + ansi-regex "^5.0.1" aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" has-unicode "^2.0.1" - signal-exit "^3.0.7" + signal-exit "^3.0.0" string-width "^4.2.3" strip-ansi "^6.0.1" - wide-align "^1.1.5" + wide-align "^1.1.2" gauge@~2.7.3: version "2.7.4" @@ -6174,9 +6221,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: - version "13.13.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" - integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== + version "13.12.1" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.1.tgz#ec206be932e6c77236677127577aa8e50bf1c5cb" + integrity sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw== dependencies: type-fest "^0.20.2" @@ -7536,9 +7583,9 @@ loose-envify@^1.1.0, loose-envify@^1.4.0: js-tokens "^3.0.0 || ^4.0.0" loupe@^2.3.1: - version "2.3.4" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" - integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== + version "2.3.3" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.3.tgz#5a92027d54cfb6de4c327d3c3b705561d394d3c6" + integrity sha512-krIV4Cf1BIGIx2t1e6tucThhrBemUnIUjMtD2vN4mrMxnxpBvrcosBSpooqunBqP/hOEEV1w/Cr1YskGtqw5Jg== dependencies: get-func-name "^2.0.0" @@ -7689,7 +7736,7 @@ markdown-it@^12.3.2: mdurl "^1.0.1" uc.micro "^1.0.5" -marked@^4.0.12: +marked@^4.0.10: version "4.0.12" resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d" integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ== @@ -7756,17 +7803,17 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +mime-db@1.51.0: + version "1.51.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.25, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + version "2.1.34" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== dependencies: - mime-db "1.52.0" + mime-db "1.51.0" mime@1.6.0, mime@^1.4.1: version "1.6.0" @@ -7815,20 +7862,20 @@ minimatch@3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.2: +minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4: + version "3.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" + integrity sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" - integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== - dependencies: - brace-expansion "^2.0.1" - minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -8082,10 +8129,10 @@ nano@^9.0.5: qs "^6.9.4" tough-cookie "^4.0.0" -nanoid@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.2.tgz#c89622fafb4381cd221421c69ec58547a1eec557" - integrity sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA== +nanoid@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== napi-build-utils@^1.0.1: version "1.0.2" @@ -8446,11 +8493,11 @@ npmlog@^4.0.1, npmlog@^4.1.2: set-blocking "~2.0.0" npmlog@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.1.tgz#06f1344a174c06e8de9c6c70834cfba2964bba17" - integrity sha512-BTHDvY6nrRHuRfyjt1MAufLxYdVXZfd099H4+i1f0lPywNQyI4foeNXJRObB/uy+TYqUW0vAD9gbdSOXPst7Eg== + version "6.0.0" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.0.tgz#ba9ef39413c3d936ea91553db7be49c34ad0520c" + integrity sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q== dependencies: - are-we-there-yet "^3.0.0" + are-we-there-yet "^2.0.0" console-control-strings "^1.1.0" gauge "^4.0.0" set-blocking "^2.0.0" @@ -9181,12 +9228,12 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.7: - version "8.4.12" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905" - integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg== +postcss@^8.4.5: + version "8.4.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" + integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== dependencies: - nanoid "^3.3.1" + nanoid "^3.2.0" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -9868,9 +9915,9 @@ request@^2.82.0, request@^2.86.0, request@^2.87.0, request@^2.88.0, request@^2.8 uuid "^3.3.2" requestretry@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/requestretry/-/requestretry-7.0.2.tgz#22a169e5d3aaadd95d00401d7347511f3695faf4" - integrity sha512-Zz8z7G2OuVs4F0wR0shKMEMm7lNvPNHM0UIHNns9qfyuBDKSExoTsZGtSjKst6nPEwlMrbA9G+m/yC0AbGj+8w== + version "7.0.0" + resolved "https://registry.yarnpkg.com/requestretry/-/requestretry-7.0.0.tgz#570a9fcbeb2d6a85d7f15eb2265840d787f0c44d" + integrity sha512-g1Odu3IBKb6fYQog+HLy5FZ1CMwejIpD0iX1u1qXLsRj8TeQmFCpX9pTe50qhIirKvx1mcmoAeuLBFXLlBw6vA== dependencies: extend "^3.0.2" lodash "^4.17.15" @@ -10218,10 +10265,10 @@ shell-quote@^1.4.3: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== -shiki@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.10.1.tgz#6f9a16205a823b56c072d0f1a0bcd0f2646bef14" - integrity sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng== +shiki@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.10.0.tgz#85f21ecfa95b377ff64db6c71442c22c220e9540" + integrity sha512-iczxaIYeBFHTFrQPb9DVy2SKgYxC4Wo7Iucm7C17cCh2Ge/refnvHscUOxM85u57MfLoNOtjoEFUWt9gBexblA== dependencies: jsonc-parser "^3.0.0" vscode-oniguruma "^1.6.1" @@ -10243,7 +10290,7 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -10330,9 +10377,9 @@ socket.io-parser@~4.0.4: debug "~4.3.1" socket.io-parser@~4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.1.2.tgz#0a97d4fb8e67022158a568450a6e41887e42035e" - integrity sha512-j3kk71QLJuyQ/hh5F/L2t1goqzdTL0gvDzuhTuNSwihfuFUrcSji0qFZmJJPtG6Rmug153eOPsUizeirf1IIog== + version "4.1.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.1.1.tgz#0ad53d980781cab1eabe320417d8480c0133e62d" + integrity sha512-USQVLSkDWE5nbcY760ExdKaJxCE65kcsG/8k5FDGZVVxpD1pA7hABYXYkCUvxUuYYh/+uQw0N/fvBzfT8o07KA== dependencies: "@socket.io/component-emitter" "~3.0.0" debug "~4.3.1" @@ -11260,15 +11307,15 @@ typedoc-plugin-external-module-map@1.3.2: integrity sha512-xs+vQ7XeVOvXlomdAw3xpwm8W59pPQPmfHTl5WvH2OkPFEO7EhABikLlTzLRYCOCEdkRg4LcnCblKmARN0wCIA== typedoc@^0.22.11: - version "0.22.13" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.13.tgz#d061f8f0fb7c9d686e48814f245bddeea4564e66" - integrity sha512-NHNI7Dr6JHa/I3+c62gdRNXBIyX7P33O9TafGLd07ur3MqzcKgwTvpg18EtvCLHJyfeSthAtCLpM7WkStUmDuQ== + version "0.22.11" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.11.tgz#a3d7f4577eef9fc82dd2e8f4e2915e69f884c250" + integrity sha512-pVr3hh6dkS3lPPaZz1fNpvcrqLdtEvXmXayN55czlamSgvEjh+57GUqfhAI1Xsuu/hNHUT1KNSx8LH2wBP/7SA== dependencies: glob "^7.2.0" lunr "^2.3.9" - marked "^4.0.12" - minimatch "^5.0.1" - shiki "^0.10.1" + marked "^4.0.10" + minimatch "^3.0.4" + shiki "^0.10.0" typescript@~4.5.5: version "4.5.5" @@ -11281,9 +11328,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== uglify-js@^3.1.4: - version "3.15.3" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471" - integrity sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg== + version "3.15.1" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.1.tgz#9403dc6fa5695a6172a91bc983ea39f0f7c9086d" + integrity sha512-FAGKF12fWdkpvNJZENacOH0e/83eG6JyVQyanIJaBXCN1J11TUQv1T1/z8S+Z0CG0ZPk1nPcreF/c7lrTd0TEQ== uid-number@0.0.6: version "0.0.6" @@ -11526,6 +11573,11 @@ vscode-jsonrpc@^5.0.0, vscode-jsonrpc@^5.0.1: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A== +vscode-jsonrpc@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== + vscode-languageserver-protocol@~3.15.3: version "3.15.3" resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz#3fa9a0702d742cf7883cb6182a6212fcd0a1d8bb" @@ -11651,12 +11703,12 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.48.0: - version "5.70.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d" - integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw== + version "5.68.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.68.0.tgz#a653a58ed44280062e47257f260117e4be90d560" + integrity sha512-zUcqaUO0772UuuW2bzaES2Zjlm/y3kRBQDVFVCge+s2Y8mwuUTdperGaAv65/NtRL/1zanpSJOq/MD8u61vo6g== dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" + "@types/eslint-scope" "^3.7.0" + "@types/estree" "^0.0.50" "@webassemblyjs/ast" "1.11.1" "@webassemblyjs/wasm-edit" "1.11.1" "@webassemblyjs/wasm-parser" "1.11.1" @@ -11664,7 +11716,7 @@ webpack@^5.48.0: acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.9.2" + enhanced-resolve "^5.8.3" es-module-lexer "^0.9.0" eslint-scope "5.1.1" events "^3.2.0" @@ -11769,7 +11821,7 @@ wide-align@1.1.3: dependencies: string-width "^1.0.2 || 2" -wide-align@^1.1.0, wide-align@^1.1.5: +wide-align@^1.1.0, wide-align@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== @@ -11968,9 +12020,9 @@ xterm-addon-search@^0.8.2: integrity sha512-I1863mjn8P6uVrqm/X+btalVsqjAKLhnhpbP7SavAOpEkI1jJhbHU2UTp7NjeRtcKTks6UWk/ycgds5snDSejg== xterm@^4.16.0: - version "4.18.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1" - integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ== + version "4.17.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.17.0.tgz#e48ba6eeb83e118ec163f5512c3cfe9dbbf3f838" + integrity sha512-WGXlIHvLvZKtwMdFaL6kUwp+c9abd2Pcakp/GmuefBuOtGCu9fP9tBDPKyL/A17N+5tt44EYk3YsBbvkPBubMw== y18n@^4.0.0: version "4.0.3" @@ -12037,9 +12089,9 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3: integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== yargs-parser@^21.0.0: - version "21.0.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" - integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== + version "21.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55" + integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA== yargs-unparser@1.6.0: version "1.6.0" @@ -12114,9 +12166,9 @@ yargs@^16.2.0: yargs-parser "^20.2.2" yargs@^17.0.1: - version "17.4.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.0.tgz#9fc9efc96bd3aa2c1240446af28499f0e7593d00" - integrity sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA== + version "17.3.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" + integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== dependencies: cliui "^7.0.2" escalade "^3.1.1"