From 49c19a5b5083b896f5e85eb4a093eef305cff796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Tue, 14 Mar 2023 17:09:48 +0100 Subject: [PATCH 1/5] Enable context isolation and disable nodejs support. Fixes #2018 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All access to electron API is now done through an API exposed via a preload script. Access to the electron API (including electron-remote) and nodejs API is no longer possible. Theia extensions can contribute to the preload script via a `theiaExtensions` module declaration in their package.json Contributed on behalf or STMicroelectronics Signed-off-by: Thomas Mäder --- CHANGELOG.md | 1 + .../src/generator/frontend-generator.ts | 13 +- .../src/generator/webpack-generator.ts | 22 +- .../src/application-package.ts | 8 + .../src/extension-package.ts | 1 + doc/Migration.md | 27 ++ .../menu/sample-electron-menu-module.ts | 17 +- .../sample-updater-frontend-contribution.ts | 11 +- packages/core/README.md | 2 - .../@electron/remote/index.d.ts | 1 - .../electron-shared/@electron/remote/index.js | 1 - .../@electron/remote/main/index.d.ts | 1 - .../@electron/remote/main/index.js | 1 - packages/core/package.json | 3 + .../electron-clipboard-service.ts | 5 +- ...lectron-keyboard-layout-change-notifier.ts | 3 +- .../menu/electron-context-menu-renderer.ts | 27 +- .../menu/electron-main-menu-factory.ts | 95 +++--- .../menu/electron-menu-contribution.ts | 113 +++---- .../electron-ipc-connection-provider.ts | 13 +- packages/core/src/electron-browser/preload.ts | 205 +++++++++++++ .../token/electron-token-frontend-module.ts | 3 +- .../electron-frontend-application-state.ts | 4 +- .../electron-secondary-window-service.ts | 38 +-- .../window/electron-window-service.ts | 33 +- .../core/src/electron-common/electron-api.ts | 131 ++++++++ .../messaging/electron-connection-handler.ts | 9 - .../messaging/electron-messages.ts | 42 --- .../src/electron-main/electron-api-main.ts | 286 ++++++++++++++++++ .../electron-main-application-module.ts | 6 +- .../electron-main-application.ts | 69 ++--- .../electron-main/electron-native-keymap.ts | 40 --- .../electron-messaging-contribution.ts | 18 +- .../electron-main/theia-electron-window.ts | 41 +-- packages/electron/README.md | 2 - packages/electron/package.json | 3 - .../shared/@electron/remote/index.d.ts | 1 - .../electron/shared/@electron/remote/index.js | 1 - .../shared/@electron/remote/main/index.d.ts | 1 - .../shared/@electron/remote/main/index.js | 1 - packages/filesystem/package.json | 4 + .../electron-file-dialog-service.ts | 169 ++++------- .../src/electron-browser/preload.ts | 31 ++ .../src/electron-common/electron-api.ts | 55 ++++ .../src/electron-main/electron-api-main.ts | 78 +++++ .../src/electron-main/electron-main-module.ts | 22 ++ .../electron-mini-browser-environment.ts | 13 +- .../electron-navigator-menu-contribution.ts | 10 +- .../electron-webview-widget-factory.ts | 19 +- yarn.lock | 5 - 50 files changed, 1147 insertions(+), 558 deletions(-) delete mode 100644 packages/core/electron-shared/@electron/remote/index.d.ts delete mode 100644 packages/core/electron-shared/@electron/remote/index.js delete mode 100644 packages/core/electron-shared/@electron/remote/main/index.d.ts delete mode 100644 packages/core/electron-shared/@electron/remote/main/index.js create mode 100644 packages/core/src/electron-browser/preload.ts create mode 100644 packages/core/src/electron-common/electron-api.ts delete mode 100644 packages/core/src/electron-common/messaging/electron-messages.ts create mode 100644 packages/core/src/electron-main/electron-api-main.ts delete mode 100644 packages/core/src/electron-main/electron-native-keymap.ts delete mode 100644 packages/electron/shared/@electron/remote/index.d.ts delete mode 100644 packages/electron/shared/@electron/remote/index.js delete mode 100644 packages/electron/shared/@electron/remote/main/index.d.ts delete mode 100644 packages/electron/shared/@electron/remote/main/index.js create mode 100644 packages/filesystem/src/electron-browser/preload.ts create mode 100644 packages/filesystem/src/electron-common/electron-api.ts create mode 100644 packages/filesystem/src/electron-main/electron-api-main.ts create mode 100644 packages/filesystem/src/electron-main/electron-main-module.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f7e5301e7605..5527fdb773840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - Interface `ScmInlineActions` removes `commands: CommandRegistry` - Interface `ScmTreeWidget.Props` removes `commands: CommandRegistry` - [terminal] removed `openTerminalFromProfile` method from `TerminalFrontendContribution` [#12322](https://github.com/eclipse-theia/theia/pull/12322) +- [electron] enabled context isolation and disabled node integration in Electron renderer (https://github.com/eclipse-theia/theia/issues/2018) ## v1.35.0 - 02/23/2023 diff --git a/dev-packages/application-manager/src/generator/frontend-generator.ts b/dev-packages/application-manager/src/generator/frontend-generator.ts index a81d391ef3e3c..2b293c68ceeae 100644 --- a/dev-packages/application-manager/src/generator/frontend-generator.ts +++ b/dev-packages/application-manager/src/generator/frontend-generator.ts @@ -25,6 +25,7 @@ export class FrontendGenerator extends AbstractGenerator { const frontendModules = this.pck.targetFrontendModules; await this.write(this.pck.frontend('index.html'), this.compileIndexHtml(frontendModules)); await this.write(this.pck.frontend('index.js'), this.compileIndexJs(frontendModules)); + await this.write(this.pck.frontend('preload.js'), this.compilePreloadJs()); await this.write(this.pck.frontend('secondary-window.html'), this.compileSecondaryWindowHtml()); await this.write(this.pck.frontend('secondary-index.js'), this.compileSecondaryIndexJs(this.pck.secondaryWindowModules)); if (this.pck.isElectron()) { @@ -133,7 +134,6 @@ module.exports = preloader.preload().then(() => { return `// @ts-check require('reflect-metadata'); -require('@theia/electron/shared/@electron/remote/main').initialize(); // Useful for Electron/NW.js apps as GUI apps on macOS doesn't inherit the \`$PATH\` define // in your dotfiles (.bashrc/.bash_profile/.zshrc/etc). @@ -266,6 +266,17 @@ module.exports = Promise.resolve().then(() => { container.load(frontendApplicationModule); ${compiledModuleImports} }); +`; + } + + compilePreloadJs(): string { + const lines = Array.from(this.pck.preloadModules) + .map(([moduleName, path]) => `require('${path}').preload();`); + const imports = '\n' + lines.join('\n'); + + return `\ +// @ts-check +${imports} `; } } diff --git a/dev-packages/application-manager/src/generator/webpack-generator.ts b/dev-packages/application-manager/src/generator/webpack-generator.ts index a96cee368b91b..69f783b182dbf 100644 --- a/dev-packages/application-manager/src/generator/webpack-generator.ts +++ b/dev-packages/application-manager/src/generator/webpack-generator.ts @@ -103,7 +103,7 @@ module.exports = [{ devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]', globalObject: 'self' }, - target: '${this.ifBrowser('web', 'electron-renderer')}', + target: 'web', cache: staticCompression, module: { rules: [ @@ -252,7 +252,7 @@ module.exports = [{ devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]', globalObject: 'self' }, - target: 'electron-renderer', + target: 'web', cache: staticCompression, module: { rules: [ @@ -278,6 +278,24 @@ module.exports = [{ warnings: true, children: true } +}, { + mode, + devtool: 'source-map', + entry: { + "preload": path.resolve(__dirname, 'src-gen/frontend/preload.js'), + }, + output: { + filename: '[name].js', + path: outputPath, + devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]', + globalObject: 'self' + }, + target: 'electron-preload', + cache: staticCompression, + stats: { + warnings: true, + children: true + } }];`; } diff --git a/dev-packages/application-package/src/application-package.ts b/dev-packages/application-package/src/application-package.ts index c406de8176f0c..28906e16685c6 100644 --- a/dev-packages/application-package/src/application-package.ts +++ b/dev-packages/application-package/src/application-package.ts @@ -96,6 +96,7 @@ export class ApplicationPackage { protected _backendModules: Map | undefined; protected _backendElectronModules: Map | undefined; protected _electronMainModules: Map | undefined; + protected _preloadModules: Map | undefined; protected _extensionPackages: ReadonlyArray | undefined; /** @@ -176,6 +177,13 @@ export class ApplicationPackage { return this._electronMainModules; } + get preloadModules(): Map { + if (!this._preloadModules) { + this._preloadModules = this.computeModules('preload'); + } + return this._preloadModules; + } + protected computeModules

(primary: P, secondary?: S): Map { const result = new Map(); let moduleIndex = 1; diff --git a/dev-packages/application-package/src/extension-package.ts b/dev-packages/application-package/src/extension-package.ts index b654d174992f6..c5ec609d4efc8 100644 --- a/dev-packages/application-package/src/extension-package.ts +++ b/dev-packages/application-package/src/extension-package.ts @@ -26,6 +26,7 @@ export interface Extension { backend?: string; backendElectron?: string; electronMain?: string; + preload?: string; } export interface ExtensionPackageOptions { diff --git a/doc/Migration.md b/doc/Migration.md index ef28721041134..26b91954eadb7 100644 --- a/doc/Migration.md +++ b/doc/Migration.md @@ -20,6 +20,33 @@ For example: } ``` + +### v1.36.0 + +#### Disabled node integration and added context isolation flag in Electron renderer + +This also means that `electron-remote` can no longer be used in components in `electron-frontend` or `electron-common`. In order to use electron-related functionality from the browser, you need to expose an API via a preload script (see https://www.electronjs.org/docs/latest/tutorial/context-isolation). to achieve this from a Theia extension, you need to follow these steps: +1. Define the API interface and declare an api variable on the global `window` variable. See `packages/filesystem/electron-common/electron-api.ts` for an example +2. Write a preload script module that implements the API on the renderer ("browser") side and exposes the API via `exposeInMainWorld`. You'll need to expose the API in an exported function called `preload()`. See `packages/filesystem/electron-browser/preload.ts` for an example. +3. Declare a `theiaExtensions` entry pointing to the preload script like so: +``` +"theiaExtensions": [ + { + "preload": "lib/electron-browser/preload", +``` +See `/packages/filesystem/package.json` for an example + +4. Implement the API on the electron-main side by contributing a `ElectronMainApplicationContribution`. See `packages/filesystem/electron-main/electron-api-main.ts` for an example. If you don't have a module contributing to the electron-main application, you may have to declare it in your package.json. +``` +"theiaExtensions": [ + { + "preload": "lib/electron-browser/preload", + "electronMain": "lib/electron-main/electron-main-module" + } +``` + +If you are using nodejs API in your electron browser-side code you will also have to move the code outside of the renderer process, for exmaple +by setting up an API like described above, or, for example, by using a back-end service. ### v1.35.0 #### Drop support for `Node 14` diff --git a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts index 9484a739698ea..2cd55bd0defed 100644 --- a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts +++ b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts @@ -15,9 +15,10 @@ // ***************************************************************************** import { injectable, ContainerModule } from '@theia/core/shared/inversify'; -import { CompoundMenuNode, MenuNode } from '@theia/core/lib/common/menu'; +import { MenuNode } from '@theia/core/lib/common/menu'; import { ElectronMainMenuFactory, ElectronMenuOptions } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; import { PlaceholderMenuNode } from '../../browser/menu/sample-menu-contribution'; +import { MenuDto } from '@theia/core/lib/electron-common/electron-api'; export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(ElectronMainMenuFactory).to(SampleElectronMainMenuFactory).inSingletonScope(); @@ -25,13 +26,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { @injectable() class SampleElectronMainMenuFactory extends ElectronMainMenuFactory { - protected override fillMenuTemplate( - parentItems: Electron.MenuItemConstructorOptions[], menuModel: MenuNode & CompoundMenuNode, args: unknown[] = [], options: ElectronMenuOptions - ): Electron.MenuItemConstructorOptions[] { - if (menuModel instanceof PlaceholderMenuNode) { - parentItems.push({ label: menuModel.label, enabled: false, visible: true }); + protected override fillMenuTemplate(parentItems: MenuDto[], + menu: MenuNode, + args: unknown[] = [], + options: ElectronMenuOptions + ): MenuDto[] { + if (menu instanceof PlaceholderMenuNode) { + parentItems.push({ label: menu.label, enabled: false, visible: true }); } else { - super.fillMenuTemplate(parentItems, menuModel, args, options); + super.fillMenuTemplate(parentItems, menu, args, options); } return parentItems; } 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..5a917a3b52f1f 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 @@ -14,10 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; -import { Menu, BrowserWindow } from '@theia/core/electron-shared/electron'; 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, @@ -91,12 +88,8 @@ export class ElectronMenuUpdater { this.setMenu(); } - private setMenu(menu: Menu | null = this.factory.createElectronMenuBar(), electronWindow: BrowserWindow = electronRemote.getCurrentWindow()): void { - if (isOSX) { - electronRemote.Menu.setApplicationMenu(menu); - } else { - electronWindow.setMenu(menu); - } + private setMenu(): void { + window.electronTheiaCore.setMenu(this.factory.createElectronMenuBar()); } } diff --git a/packages/core/README.md b/packages/core/README.md index 2dcc324e00412..d77b1d79e6457 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -69,8 +69,6 @@ export class SomeClass { ## Re-Exports - `@theia/core/electron-shared/...` - - `@electron/remote` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote)) - - `@electron/remote/main` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote)) - `native-keymap` (from [`native-keymap@^2.2.1`](https://www.npmjs.com/package/native-keymap)) - `electron` (from [`electron@^15.3.5`](https://www.npmjs.com/package/electron)) - `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store)) diff --git a/packages/core/electron-shared/@electron/remote/index.d.ts b/packages/core/electron-shared/@electron/remote/index.d.ts deleted file mode 100644 index 146dfc4d862f5..0000000000000 --- a/packages/core/electron-shared/@electron/remote/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@theia/electron/shared/@electron/remote'; diff --git a/packages/core/electron-shared/@electron/remote/index.js b/packages/core/electron-shared/@electron/remote/index.js deleted file mode 100644 index 1a0318e4169e5..0000000000000 --- a/packages/core/electron-shared/@electron/remote/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@theia/electron/shared/@electron/remote'); diff --git a/packages/core/electron-shared/@electron/remote/main/index.d.ts b/packages/core/electron-shared/@electron/remote/main/index.d.ts deleted file mode 100644 index 283d7e0b4d57e..0000000000000 --- a/packages/core/electron-shared/@electron/remote/main/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@theia/electron/shared/@electron/remote/main'; diff --git a/packages/core/electron-shared/@electron/remote/main/index.js b/packages/core/electron-shared/@electron/remote/main/index.js deleted file mode 100644 index ec85b8feab3e2..0000000000000 --- a/packages/core/electron-shared/@electron/remote/main/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@theia/electron/shared/@electron/remote/main'); diff --git a/packages/core/package.json b/packages/core/package.json index 2338f07621729..bc2eb42349eed 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -129,6 +129,9 @@ } }, "theiaExtensions": [ + { + "preload": "lib/electron-browser/preload" + }, { "frontend": "lib/browser/i18n/i18n-frontend-module", "backend": "lib/node/i18n/i18n-backend-module" diff --git a/packages/core/src/electron-browser/electron-clipboard-service.ts b/packages/core/src/electron-browser/electron-clipboard-service.ts index 40a53f6baa94b..79e5c728db7f1 100644 --- a/packages/core/src/electron-browser/electron-clipboard-service.ts +++ b/packages/core/src/electron-browser/electron-clipboard-service.ts @@ -15,7 +15,6 @@ // ***************************************************************************** // eslint-disable-next-line import/no-extraneous-dependencies -import { clipboard } from 'electron'; import { injectable } from 'inversify'; import { ClipboardService } from '../browser/clipboard-service'; @@ -23,11 +22,11 @@ import { ClipboardService } from '../browser/clipboard-service'; export class ElectronClipboardService implements ClipboardService { readText(): string { - return clipboard.readText(); + return window.electronTheiaCore.readClipboard(); } writeText(value: string): void { - clipboard.writeText(value); + window.electronTheiaCore.writeClipboard(value); } } diff --git a/packages/core/src/electron-browser/keyboard/electron-keyboard-layout-change-notifier.ts b/packages/core/src/electron-browser/keyboard/electron-keyboard-layout-change-notifier.ts index 7d38064c5e39c..651e2ecdecdf5 100644 --- a/packages/core/src/electron-browser/keyboard/electron-keyboard-layout-change-notifier.ts +++ b/packages/core/src/electron-browser/keyboard/electron-keyboard-layout-change-notifier.ts @@ -14,7 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { ipcRenderer } from '@theia/electron/shared/electron'; import { postConstruct, injectable } from 'inversify'; import { KeyboardLayoutChangeNotifier, NativeKeyboardLayout } from '../../common/keyboard/keyboard-layout-provider'; import { Emitter, Event } from '../../common/event'; @@ -34,7 +33,7 @@ export class ElectronKeyboardLayoutChangeNotifier implements KeyboardLayoutChang @postConstruct() protected initialize(): void { - ipcRenderer.on('keyboardLayoutChanged', (event: Electron.IpcRendererEvent, newLayout: NativeKeyboardLayout) => this.nativeLayoutChanged.fire(newLayout)); + window.electronTheiaCore.onKeyboardLayoutChanged((newLayout: NativeKeyboardLayout) => this.nativeLayoutChanged.fire(newLayout)); } } diff --git a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts index dad25dca2d646..93e6ed9892f49 100644 --- a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts +++ b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts @@ -16,7 +16,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as electron from '../../../electron-shared/electron'; import { inject, injectable, postConstruct } from 'inversify'; import { ContextMenuRenderer, RenderContextMenuOptions, ContextMenuAccess, FrontendApplicationContribution, CommonCommands, coordinateFromAnchor, PreferenceService @@ -25,12 +24,11 @@ import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { ContextMenuContext } from '../../browser/menu/context-menu-context'; import { MenuPath, MenuContribution, MenuModelRegistry } from '../../common'; import { BrowserContextMenuRenderer } from '../../browser/menu/browser-context-menu-renderer'; -import { RequestTitleBarStyle, TitleBarStyleAtStartup } from '../../electron-common/messaging/electron-messages'; export class ElectronContextMenuAccess extends ContextMenuAccess { - constructor(readonly menu: electron.Menu) { + constructor(readonly menuHandle: Promise) { super({ - dispose: () => menu.closePopup() + dispose: () => menuHandle.then(handle => window.electronTheiaCore.closePopup(handle)) }); } } @@ -93,10 +91,7 @@ export class ElectronContextMenuRenderer extends BrowserContextMenuRenderer { @postConstruct() protected async init(): Promise { - electron.ipcRenderer.on(TitleBarStyleAtStartup, (_event, style: string) => { - this.useNativeStyle = style === 'native'; - }); - electron.ipcRenderer.send(RequestTitleBarStyle); + this.useNativeStyle = await window.electronTheiaCore.getTitleBarStyleAtStartup() === 'native'; } protected override doRender(options: RenderContextMenuOptions): ContextMenuAccess { @@ -104,17 +99,15 @@ export class ElectronContextMenuRenderer extends BrowserContextMenuRenderer { const { menuPath, anchor, args, onHide, context, contextKeyService } = options; const menu = this.electronMenuFactory.createElectronContextMenu(menuPath, args, context, contextKeyService); const { x, y } = coordinateFromAnchor(anchor); - const zoom = electron.webFrame.getZoomFactor(); - // TODO: Remove the offset once Electron fixes https://github.com/electron/electron/issues/31641 - const offset = process.platform === 'win32' ? 0 : 2; - // x and y values must be Ints or else there is a conversion error - menu.popup({ x: Math.round(x * zoom) + offset, y: Math.round(y * zoom) + offset }); + + const menuHandle = window.electronTheiaCore.popup(menu, x, y, () => { + if (onHide) { + onHide(); + } + }); // native context menu stops the event loop, so there is no keyboard events this.context.resetAltPressed(); - if (onHide) { - menu.once('menu-will-close', () => onHide()); - } - return new ElectronContextMenuAccess(menu); + return new ElectronContextMenuAccess(menuHandle); } else { return super.doRender(options); } diff --git a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts index ab99319a32190..723070d0e5115 100644 --- a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts +++ b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts @@ -16,7 +16,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { inject, injectable, postConstruct } from 'inversify'; import { isOSX, MAIN_MENU_BAR, MenuPath, MenuNode, CommandMenuNode, CompoundMenuNode, CompoundMenuNodeRole } from '../../common'; import { Keybinding } from '../../common/keybinding'; @@ -24,7 +23,8 @@ import { PreferenceService, CommonCommands } from '../../browser'; import debounce = require('lodash.debounce'); import { MAXIMIZED_CLASS } from '../../browser/shell/theia-dock-panel'; import { BrowserMainMenuFactory } from '../../browser/menu/browser-menu-plugin'; -import { ContextMatcher } from 'src/browser/context-key-service'; +import { ContextMatcher } from '../../browser/context-key-service'; +import { MenuDto, MenuRole } from '../../electron-common/electron-api'; /** * Representation of possible electron menu options. @@ -68,7 +68,7 @@ export type ElectronMenuItemRole = ('undo' | 'redo' | 'cut' | 'copy' | 'paste' | @injectable() export class ElectronMainMenuFactory extends BrowserMainMenuFactory { - protected _menu?: Electron.Menu; + protected _menu?: MenuDto[]; protected _toggledCommands: Set = new Set(); @inject(PreferenceService) @@ -82,13 +82,13 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { this.setMenuBar(); } if (this._menu) { - for (const item of this._toggledCommands) { - const menuItem = this._menu.getMenuItemById(item); + for (const cmd of this._toggledCommands) { + const menuItem = this.findMenuById(this._menu, cmd); if (menuItem) { - menuItem.checked = this.commandRegistry.isToggled(item); + menuItem.checked = this.commandRegistry.isToggled(cmd); } } - electronRemote.getCurrentWindow().setMenu(this._menu); + window.electronTheiaCore.setMenu(this._menu); } }, 10) ); @@ -99,56 +99,49 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { async setMenuBar(): Promise { await this.preferencesService.ready; - if (isOSX) { - const createdMenuBar = this.createElectronMenuBar(); - electronRemote.Menu.setApplicationMenu(createdMenuBar); - } else if (this.preferencesService.get('window.titleBarStyle') === 'native') { - const createdMenuBar = this.createElectronMenuBar(); - electronRemote.getCurrentWindow().setMenu(createdMenuBar); - } + const createdMenuBar = this.createElectronMenuBar(); + window.electronTheiaCore.setMenu(createdMenuBar); } - createElectronMenuBar(): Electron.Menu | null { + createElectronMenuBar(): MenuDto[] | undefined { const preference = this.preferencesService.get('window.menuBarVisibility') || 'classic'; const maxWidget = document.getElementsByClassName(MAXIMIZED_CLASS); if (preference === 'visible' || (preference === 'classic' && maxWidget.length === 0)) { const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); - const template = this.fillMenuTemplate([], menuModel, [], { rootMenuPath: MAIN_MENU_BAR }); + this._menu = this.fillMenuTemplate([], menuModel, [], { rootMenuPath: MAIN_MENU_BAR }); if (isOSX) { - template.unshift(this.createOSXMenu()); - } - const menu = electronRemote.Menu.buildFromTemplate(template); - if (!menu) { - throw new Error('menu is null'); + this._menu.unshift(this.createOSXMenu()); } - this._menu = menu; return this._menu; } this._menu = undefined; // eslint-disable-next-line no-null/no-null - return null; + return undefined; } - createElectronContextMenu(menuPath: MenuPath, args?: any[], context?: HTMLElement, contextKeyService?: ContextMatcher): Electron.Menu { + createElectronContextMenu(menuPath: MenuPath, args?: any[], context?: HTMLElement, contextKeyService?: ContextMatcher): MenuDto[] { const menuModel = this.menuProvider.getMenu(menuPath); - const template = this.fillMenuTemplate([], menuModel, args, { showDisabled: false, context, rootMenuPath: menuPath, contextKeyService }); - return electronRemote.Menu.buildFromTemplate(template); + return this.fillMenuTemplate([], menuModel, args, { showDisabled: false, context, rootMenuPath: menuPath, contextKeyService }); } - protected fillMenuTemplate(parentItems: Electron.MenuItemConstructorOptions[], + protected fillMenuTemplate(parentItems: MenuDto[], menu: MenuNode, args: unknown[] = [], options: ElectronMenuOptions - ): Electron.MenuItemConstructorOptions[] { + ): MenuDto[] { const showDisabled = options?.showDisabled !== false; if (CompoundMenuNode.is(menu) && menu.children.length && this.undefinedOrMatch(options.contextKeyService ?? this.contextKeyService, menu.when, options.context)) { const role = CompoundMenuNode.getRole(menu); - if (role === CompoundMenuNodeRole.Group && menu.id === 'inline') { return parentItems; } + if (role === CompoundMenuNodeRole.Group && menu.id === 'inline') { + return parentItems; + } const children = CompoundMenuNode.getFlatChildren(menu.children); - const myItems: Electron.MenuItemConstructorOptions[] = []; + const myItems: MenuDto[] = []; children.forEach(child => this.fillMenuTemplate(myItems, child, args, options)); - if (myItems.length === 0) { return parentItems; } + if (myItems.length === 0) { + return parentItems; + } if (role === CompoundMenuNodeRole.Submenu) { parentItems.push({ label: menu.label, submenu: myItems }); } else if (role === CompoundMenuNodeRole.Group && menu.id !== 'inline') { @@ -183,7 +176,7 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { const accelerator = bindings[0] && this.acceleratorFor(bindings[0]); - const menuItem: Electron.MenuItemConstructorOptions = { + const menuItem: MenuDto = { id: node.id, label: node.label, type: this.commandRegistry.getToggledHandler(commandId, ...args) ? 'checkbox' : 'normal', @@ -191,14 +184,14 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { enabled: true, // https://github.com/eclipse-theia/theia/issues/446 visible: true, accelerator, - click: () => this.execute(commandId, args, options.rootMenuPath) + execute: () => this.execute(commandId, args, options.rootMenuPath) }; if (isOSX) { const role = this.roleFor(node.id); if (role) { menuItem.role = role; - delete menuItem.click; + delete menuItem.execute; } } parentItems.push(menuItem); @@ -235,8 +228,8 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { return this.keybindingRegistry.acceleratorForKeyCode(keyCode, '+', true); } - protected roleFor(id: string): ElectronMenuItemRole | undefined { - let role: ElectronMenuItemRole | undefined; + protected roleFor(id: string): MenuRole | undefined { + let role: MenuRole | undefined; switch (id) { case CommonCommands.UNDO.id: role = 'undo'; @@ -262,18 +255,18 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { return role; } - protected async execute(command: string, args: any[], menuPath: MenuPath): Promise { + protected async execute(cmd: string, args: any[], menuPath: MenuPath): Promise { try { // This is workaround for https://github.com/eclipse-theia/theia/issues/446. // Electron menus do not update based on the `isEnabled`, `isVisible` property of the command. // We need to check if we can execute it. - if (this.menuCommandExecutor.isEnabled(menuPath, command, ...args)) { - await this.menuCommandExecutor.executeCommand(menuPath, command, ...args); - if (this._menu && this.menuCommandExecutor.isVisible(menuPath, command, ...args)) { - const item = this._menu.getMenuItemById(command); + if (this.menuCommandExecutor.isEnabled(menuPath, cmd, ...args)) { + await this.menuCommandExecutor.executeCommand(menuPath, cmd, ...args); + if (this._menu && this.menuCommandExecutor.isVisible(menuPath, cmd, ...args)) { + const item = this.findMenuById(this._menu, cmd); if (item) { - item.checked = this.menuCommandExecutor.isToggled(menuPath, command, ...args); - electronRemote.getCurrentWindow().setMenu(this._menu); + item.checked = this.menuCommandExecutor.isToggled(menuPath, cmd, ...args); + window.electronTheiaCore.setMenu(this._menu); } } } @@ -281,8 +274,22 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { // no-op } } + findMenuById(items: MenuDto[], id: string): MenuDto | undefined { + for (const item of items) { + if (item.id === id) { + return item; + } + if (item.submenu) { + const found = this.findMenuById(item.submenu, id); + if (found) { + return found; + } + } + } + return undefined; + } - protected createOSXMenu(): Electron.MenuItemConstructorOptions { + protected createOSXMenu(): MenuDto { return { label: 'Theia', submenu: [ diff --git a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts index 590a136c449a1..484f9f3e88d3f 100644 --- a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts +++ b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts @@ -14,8 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electron from '../../../electron-shared/electron'; -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { inject, injectable, postConstruct } from 'inversify'; import { Command, CommandContribution, CommandRegistry, isOSX, isWindows, MenuModelRegistry, MenuContribution, Disposable, nls } from '../../common'; import { @@ -25,13 +23,13 @@ import { import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { FrontendApplicationStateService, FrontendApplicationState } from '../../browser/frontend-application-state'; import { FrontendApplicationConfigProvider } from '../../browser/frontend-application-config-provider'; -import { RequestTitleBarStyle, Restart, TitleBarStyleAtStartup, TitleBarStyleChanged } from '../../electron-common/messaging/electron-messages'; import { ZoomLevel } from '../window/electron-window-preferences'; import { BrowserMenuBarContribution } from '../../browser/menu/browser-menu-plugin'; import { WindowService } from '../../browser/window/window-service'; import { WindowTitleService } from '../../browser/window/window-title-service'; import '../../../src/electron-browser/menu/electron-menu-style.css'; +import { MenuDto } from '../../electron-common/electron-api'; export namespace ElectronCommands { export const TOGGLE_DEVELOPER_TOOLS = Command.toDefaultLocalizedCommand({ @@ -131,38 +129,38 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme // OSX: Recreate the menus when changing windows. // OSX only has one menu bar for all windows, so we need to swap // between them as the user switches windows. - const targetWindow = electronRemote.getCurrentWindow(); - const callback = () => this.setMenu(app); - targetWindow.on('focus', callback); - window.addEventListener('unload', () => targetWindow.off('focus', callback)); + const disposeHandler = window.electronTheiaCore.onWindowEvent('focus', () => { + this.setMenu(app); + }); + window.addEventListener('unload', () => disposeHandler.dispose()); } protected attachMenuBarVisibilityListener(): void { this.preferenceService.onPreferenceChanged(e => { if (e.preferenceName === 'window.menuBarVisibility') { - const targetWindow = electronRemote.getCurrentWindow(); - this.handleFullScreen(targetWindow, e.newValue); + this.handleFullScreen(e.newValue); } }); } handleTitleBarStyling(app: FrontendApplication): void { this.hideTopPanel(app); - electron.ipcRenderer.on(TitleBarStyleAtStartup, (_event, style: string) => { + window.electronTheiaCore.getTitleBarStyleAtStartup().then(style => { this.titleBarStyle = style; this.setMenu(app); this.preferenceService.ready.then(() => { this.preferenceService.set('window.titleBarStyle', this.titleBarStyle, PreferenceScope.User); }); }); - electron.ipcRenderer.send(RequestTitleBarStyle); + this.preferenceService.ready.then(() => { - electronRemote.getCurrentWindow().setMenuBarVisibility(['classic', 'visible'].includes(this.preferenceService.get('window.menuBarVisibility', 'classic'))); + window.electronTheiaCore.setMenuBarVisible(['classic', 'visible'].includes(this.preferenceService.get('window.menuBarVisibility', 'classic'))); }); + this.preferenceService.onPreferenceChanged(change => { if (change.preferenceName === 'window.titleBarStyle') { - if (this.titleBarStyleChangeFlag && this.titleBarStyle !== change.newValue && electronRemote.getCurrentWindow().isFocused()) { - electron.ipcRenderer.send(TitleBarStyleChanged, change.newValue); + if (this.titleBarStyleChangeFlag && this.titleBarStyle !== change.newValue) { + window.electronTheiaCore.setTitleBarStyle(change.newValue); this.handleRequiredRestart(); } this.titleBarStyleChangeFlag = true; @@ -197,21 +195,18 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme } } - protected setMenu(app: FrontendApplication, electronMenu: electron.Menu | null = this.factory.createElectronMenuBar(), - electronWindow: electron.BrowserWindow = electronRemote.getCurrentWindow()): void { - if (isOSX) { - electronRemote.Menu.setApplicationMenu(electronMenu); - } else { + protected setMenu(app: FrontendApplication, electronMenu: MenuDto[] | undefined = this.factory.createElectronMenuBar()): void { + if (!isOSX) { this.hideTopPanel(app); if (this.titleBarStyle === 'custom' && !this.menuBar) { - this.createCustomTitleBar(app, electronWindow); + this.createCustomTitleBar(app); + return; } - // Unix/Windows: Set the per-window menus - electronWindow.setMenu(electronMenu); } + window.electronTheiaCore.setMenu(electronMenu); } - protected createCustomTitleBar(app: FrontendApplication, electronWindow: electron.BrowserWindow): void { + protected createCustomTitleBar(app: FrontendApplication): void { const dragPanel = new Widget(); dragPanel.id = 'theia-drag-panel'; app.shell.addWidget(dragPanel, { area: 'top' }); @@ -220,13 +215,13 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme const controls = document.createElement('div'); controls.id = 'window-controls'; controls.append( - this.createControlButton('minimize', () => electronWindow.minimize()), - this.createControlButton('maximize', () => electronWindow.maximize()), - this.createControlButton('restore', () => electronWindow.unmaximize()), - this.createControlButton('close', () => electronWindow.close()) + this.createControlButton('minimize', () => window.electronTheiaCore.minimize()), + this.createControlButton('maximize', () => window.electronTheiaCore.maximize()), + this.createControlButton('restore', () => window.electronTheiaCore.unMaximize()), + this.createControlButton('close', () => window.electronTheiaCore.close()) ); app.shell.topPanel.node.append(controls); - this.handleWindowControls(electronWindow); + this.handleWindowControls(); } protected createCustomTitleWidget(app: FrontendApplication): void { @@ -236,18 +231,14 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme } } - protected handleWindowControls(electronWindow: electron.BrowserWindow): void { - toggleControlButtons(); - electronWindow.on('maximize', toggleControlButtons); - electronWindow.on('unmaximize', toggleControlButtons); + protected handleWindowControls(): void { + window.electronTheiaCore.onWindowEvent('maximize', () => { + document.body.classList.add('maximized'); + }); - function toggleControlButtons(): void { - if (electronWindow.isMaximized()) { - document.body.classList.add('maximized'); - } else { - document.body.classList.remove('maximized'); - } - } + window.electronTheiaCore.onWindowEvent('unmaximize', () => { + document.body.classList.remove('maximized'); + }); } protected createControlButton(id: string, handler: () => void): HTMLElement { @@ -275,22 +266,15 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme }); if (await dialog.open()) { this.windowService.setSafeToShutDown(); - electron.ipcRenderer.send(Restart); + window.electronTheiaCore.restart(); } } registerCommands(registry: CommandRegistry): void { - const currentWindow = electronRemote.getCurrentWindow(); - registry.registerCommand(ElectronCommands.TOGGLE_DEVELOPER_TOOLS, { execute: () => { - const webContent = electronRemote.getCurrentWebContents(); - if (!webContent.isDevToolsOpened()) { - webContent.openDevTools(); - } else { - webContent.closeDevTools(); - } + window.electronTheiaCore.toggleDevTools(); } }); @@ -298,14 +282,14 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme execute: () => this.windowService.reload() }); registry.registerCommand(ElectronCommands.CLOSE_WINDOW, { - execute: () => currentWindow.close() + execute: () => window.electronTheiaCore.close() }); registry.registerCommand(ElectronCommands.ZOOM_IN, { - execute: () => { - const webContents = currentWindow.webContents; + execute: async () => { + const curentLevel = await window.electronTheiaCore.getZoomLevel(); // When starting at a level that is not a multiple of 0.5, increment by at most 0.5 to reach the next highest multiple of 0.5. - let zoomLevel = (Math.floor(webContents.zoomLevel / ZoomLevel.VARIATION) * ZoomLevel.VARIATION) + ZoomLevel.VARIATION; + let zoomLevel = (Math.floor(curentLevel / ZoomLevel.VARIATION) * ZoomLevel.VARIATION) + ZoomLevel.VARIATION; if (zoomLevel > ZoomLevel.MAX) { zoomLevel = ZoomLevel.MAX; return; @@ -314,10 +298,10 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme } }); registry.registerCommand(ElectronCommands.ZOOM_OUT, { - execute: () => { - const webContents = currentWindow.webContents; + execute: async () => { + const curentLevel = await window.electronTheiaCore.getZoomLevel(); // When starting at a level that is not a multiple of 0.5, decrement by at most 0.5 to reach the next lowest multiple of 0.5. - let zoomLevel = (Math.ceil(webContents.zoomLevel / ZoomLevel.VARIATION) * ZoomLevel.VARIATION) - ZoomLevel.VARIATION; + let zoomLevel = (Math.ceil(curentLevel / ZoomLevel.VARIATION) * ZoomLevel.VARIATION) - ZoomLevel.VARIATION; if (zoomLevel < ZoomLevel.MIN) { zoomLevel = ZoomLevel.MIN; return; @@ -328,10 +312,11 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme registry.registerCommand(ElectronCommands.RESET_ZOOM, { execute: () => this.preferenceService.set('window.zoomLevel', ZoomLevel.DEFAULT, PreferenceScope.User) }); + registry.registerCommand(ElectronCommands.TOGGLE_FULL_SCREEN, { - isEnabled: () => currentWindow.isFullScreenable(), - isVisible: () => currentWindow.isFullScreenable(), - execute: () => this.toggleFullScreen(currentWindow) + isEnabled: () => window.electronTheiaCore.isFullScreenable(), + isVisible: () => window.electronTheiaCore.isFullScreenable(), + execute: () => this.toggleFullScreen() }); } @@ -408,16 +393,16 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme }); } - protected toggleFullScreen(currentWindow: electron.BrowserWindow): void { - currentWindow.setFullScreen(!currentWindow.isFullScreen()); + protected toggleFullScreen(): void { + window.electronTheiaCore.toggleFullScreen(); const menuBarVisibility = this.preferenceService.get('window.menuBarVisibility', 'classic'); - this.handleFullScreen(currentWindow, menuBarVisibility); + this.handleFullScreen(menuBarVisibility); } - protected handleFullScreen(currentWindow: electron.BrowserWindow, menuBarVisibility: string): void { - const shouldShowTop = !currentWindow.isFullScreen() || menuBarVisibility === 'visible'; + protected handleFullScreen(menuBarVisibility: string): void { + const shouldShowTop = !window.electronTheiaCore.isFullScreen() || menuBarVisibility === 'visible'; if (this.titleBarStyle === 'native') { - currentWindow.menuBarVisible = shouldShowTop; + window.electronTheiaCore.setMenuBarVisible(shouldShowTop); } else if (shouldShowTop) { this.shell.topPanel.show(); } else { 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 index 7e4410e6be60d..b5eb8f930c3f8 100644 --- a/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts +++ b/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts @@ -14,12 +14,10 @@ // 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 { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; -import { THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../../electron-common/messaging/electron-connection-handler'; -import { AbstractChannel, Channel, Disposable, WriteBuffer } from '../../common'; +import { AbstractChannel, Channel, WriteBuffer } from '../../common'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer'; export interface ElectronIpcOptions { @@ -50,16 +48,13 @@ export class ElectronIpcRendererChannel extends AbstractChannel { constructor() { super(); - const ipcMessageHandler = (_event: ElectronEvent, data: Uint8Array) => this.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(data)); - ipcRenderer.on(THEIA_ELECTRON_IPC_CHANNEL_NAME, ipcMessageHandler); - this.toDispose.push(Disposable.create(() => ipcRenderer.removeListener(THEIA_ELECTRON_IPC_CHANNEL_NAME, ipcMessageHandler))); + this.toDispose.push(window.electronTheiaCore.onData(data => this.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(data)))); + } getWriteBuffer(): WriteBuffer { const writer = new Uint8ArrayWriteBuffer(); - writer.onCommit(buffer => - ipcRenderer.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, buffer) - ); + writer.onCommit(buffer => window.electronTheiaCore.sendData(buffer)); return writer; } diff --git a/packages/core/src/electron-browser/preload.ts b/packages/core/src/electron-browser/preload.ts new file mode 100644 index 0000000000000..6df8c195e4b56 --- /dev/null +++ b/packages/core/src/electron-browser/preload.ts @@ -0,0 +1,205 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 '../common/disposable'; +import { StopReason } from '../common/frontend-application-state'; +import { NativeKeyboardLayout } from '../common/keyboard/keyboard-layout-provider'; +import { + CHANNEL_ATTACH_SECURITY_TOKEN, + CHANNEL_FOCUS_WINDOW, CHANNEL_GET_SECURITY_TOKEN, CHANNEL_INVOKE_MENU, CHANNEL_SET_MENU, CHANNEL_OPEN_POPUP, CHANNEL_CLOSE_POPUP, + MenuDto, TheiaCoreAPI, CHANNEL_ON_CLOSE_POPUP, CHANNEL_GET_TITLE_STYLE_AT_STARTUP, WindowEvent, + CHANNEL_MINIMIZE, CHANNEL_MAXIMIZE, CHANNEL_UNMAXIMIZE, CHANNEL_CLOSE, CHANNEL_TOGGLE_DEVTOOLS, + CHANNEL_ON_WINDOW_EVENT, CHANNEL_GET_ZOOM_LEVEL, CHANNEL_SET_ZOOM_LEVEL, CHANNEL_IS_FULL_SCREENABLE, CHANNEL_TOGGLE_FULL_SCREEN, + CHANNEL_IS_FULL_SCREEN, CHANNEL_SET_MENU_BAR_VISIBLE, CHANNEL_REQUEST_CLOSE, CHANNEL_SET_TITLE_STYLE, CHANNEL_RESTART, + CHANNEL_REQUEST_RELOAD, CHANNEL_APP_STATE_CHANGED, CHANNEL_SHOW_ITEM_IN_FOLDER, CHANNEL_READ_CLIPBOARD, CHANNEL_WRITE_CLIPBOARD, + CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto +} from '../electron-common/electron-api'; + +// eslint-disable-next-line import/no-extraneous-dependencies +const { ipcRenderer, contextBridge } = require('electron'); + +// a map of menuId => map handler> +const commandHandlers = new Map void>>(); +let nextHandlerId = 0; +const mainMenuId = 0; +let nextMenuId = mainMenuId + 1; + +function convertMenu(menu: MenuDto[] | undefined, handlerMap: Map void>): InternalMenuDto[] | undefined { + if (!menu) { + return undefined; + } + + return menu.map(item => { + let handlerId = undefined; + if (item.execute) { + handlerId = nextHandlerId++; + handlerMap.set(handlerId, item.execute); + } + + return { + id: item.id, + submenu: convertMenu(item.submenu, handlerMap), + accelerator: item.accelerator, + label: item.label, + handlerId: handlerId, + checked: item.checked, + enabled: item.enabled, + role: item.role, + type: item.type, + visible: item.visible + }; + }); +} + +const api: TheiaCoreAPI = { + setMenuBarVisible: (visible: boolean, windowName?: string) => ipcRenderer.send(CHANNEL_SET_MENU_BAR_VISIBLE, visible, windowName), + setMenu: (menu: MenuDto[] | undefined) => { + commandHandlers.delete(mainMenuId); + const handlers = new Map void>(); + commandHandlers.set(mainMenuId, handlers); + ipcRenderer.send(CHANNEL_SET_MENU, mainMenuId, convertMenu(menu, handlers)); + }, + getSecurityToken: () => ipcRenderer.invoke(CHANNEL_GET_SECURITY_TOKEN), + focusWindow: (name: string) => ipcRenderer.send(CHANNEL_FOCUS_WINDOW, name), + showItemInFolder: fsPath => { + ipcRenderer.send(CHANNEL_SHOW_ITEM_IN_FOLDER, fsPath); + }, + attachSecurityToken: (endpoint: string) => ipcRenderer.invoke(CHANNEL_ATTACH_SECURITY_TOKEN, endpoint), + + popup: async function (menu: MenuDto[], x: number, y: number, onClosed: () => void): Promise { + const menuId = nextMenuId++; + const handlers = new Map void>(); + commandHandlers.set(menuId, handlers); + const handle = await ipcRenderer.invoke(CHANNEL_OPEN_POPUP, menuId, convertMenu(menu, handlers), x, y); + const closeListener = () => { + ipcRenderer.removeListener(CHANNEL_ON_CLOSE_POPUP, closeListener); + commandHandlers.delete(menuId); + onClosed(); + }; + ipcRenderer.on(CHANNEL_ON_CLOSE_POPUP, closeListener); + return handle; + }, + closePopup: function (handle: number): void { + ipcRenderer.send(CHANNEL_CLOSE_POPUP, handle); + }, + getTitleBarStyleAtStartup: function (): Promise { + return ipcRenderer.invoke(CHANNEL_GET_TITLE_STYLE_AT_STARTUP); + }, + setTitleBarStyle: function (style): void { + ipcRenderer.send(CHANNEL_SET_TITLE_STYLE, style); + }, + minimize: function (): void { + ipcRenderer.send(CHANNEL_MINIMIZE); + }, + maximize: function (): void { + ipcRenderer.send(CHANNEL_MAXIMIZE); + }, + unMaximize: function (): void { + ipcRenderer.send(CHANNEL_UNMAXIMIZE); + }, + close: function (): void { + ipcRenderer.send(CHANNEL_CLOSE); + }, + onWindowEvent: function (event: WindowEvent, handler: () => void): Disposable { + const h = (_event: unknown, evt: WindowEvent) => { + if (event === evt) { + handler(); + } + }; + ipcRenderer.on(CHANNEL_ON_WINDOW_EVENT, h); + return Disposable.create(() => ipcRenderer.off(CHANNEL_ON_WINDOW_EVENT, h)); + }, + setCloseRequestHandler: function (handler: (stopReason: StopReason) => Promise): void { + ipcRenderer.on(CHANNEL_REQUEST_CLOSE, async (event, stopReason, confirmChannel, cancelChannel) => { + try { + if (await handler(stopReason)) { + event.sender.send(confirmChannel); + return; + }; + } catch (e) { + console.warn('exception in close handler ', e); + } + event.sender.send(confirmChannel); + }); + }, + + toggleDevTools: function (): void { + ipcRenderer.send(CHANNEL_TOGGLE_DEVTOOLS); + }, + getZoomLevel: function (): Promise { + return ipcRenderer.invoke(CHANNEL_GET_ZOOM_LEVEL); + }, + + setZoomLevel: function (desired: number): void { + ipcRenderer.send(CHANNEL_SET_ZOOM_LEVEL, desired); + }, + isFullScreenable: function (): boolean { + return ipcRenderer.sendSync(CHANNEL_IS_FULL_SCREENABLE); + }, + + isFullScreen: function (): boolean { + return ipcRenderer.sendSync(CHANNEL_IS_FULL_SCREEN); + + }, + toggleFullScreen: function (): void { + ipcRenderer.send(CHANNEL_TOGGLE_FULL_SCREEN); + }, + + requestReload: () => ipcRenderer.send(CHANNEL_REQUEST_RELOAD), + restart: () => ipcRenderer.send(CHANNEL_RESTART), + + applicationStateChanged: state => { + ipcRenderer.send(CHANNEL_APP_STATE_CHANGED, state); + }, + + readClipboard(): string { + return ipcRenderer.sendSync(CHANNEL_READ_CLIPBOARD); + }, + + writeClipboard(text): void { + ipcRenderer.send(CHANNEL_WRITE_CLIPBOARD, text); + }, + + onKeyboardLayoutChanged(handler): Disposable { + return createDisposableListener(CHANNEL_KEYBOARD_LAYOUT_CHANGED, (event, layout) => { handler(layout as NativeKeyboardLayout); }); + }, + + onData: handler => createDisposableListener(CHANNEL_IPC_CONNECTION, (event, data) => { handler(data as Uint8Array); }), + + sendData: data => { + ipcRenderer.send(CHANNEL_IPC_CONNECTION, data); + }, +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createDisposableListener(channel: string, handler: (event: any, ...args: unknown[]) => any): Disposable { + ipcRenderer.on(channel, handler); + return Disposable.create(() => ipcRenderer.off(channel, handler)); +} + +export function preload(): void { + console.log('exposing theia core electron api'); + ipcRenderer.on(CHANNEL_INVOKE_MENU, (_, menuId: number, handlerId: number) => { + const map = commandHandlers.get(menuId); + if (map) { + const handler = map.get(handlerId); + if (handler) { + handler(); + } + } + }); + + contextBridge.exposeInMainWorld('electronTheiaCore', api); +} diff --git a/packages/core/src/electron-browser/token/electron-token-frontend-module.ts b/packages/core/src/electron-browser/token/electron-token-frontend-module.ts index 4061023ac503c..3dd4e1db83367 100644 --- a/packages/core/src/electron-browser/token/electron-token-frontend-module.ts +++ b/packages/core/src/electron-browser/token/electron-token-frontend-module.ts @@ -14,10 +14,9 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { ContainerModule } from 'inversify'; import { ElectronSecurityToken } from '../../electron-common/electron-token'; export default new ContainerModule(bind => { - bind(ElectronSecurityToken).toConstantValue(electronRemote.getGlobal(ElectronSecurityToken)); + bind(ElectronSecurityToken).toConstantValue(window.electronTheiaCore.getSecurityToken()); }); diff --git a/packages/core/src/electron-browser/window/electron-frontend-application-state.ts b/packages/core/src/electron-browser/window/electron-frontend-application-state.ts index 6d7b7a34fa13a..d56e80076481e 100644 --- a/packages/core/src/electron-browser/window/electron-frontend-application-state.ts +++ b/packages/core/src/electron-browser/window/electron-frontend-application-state.ts @@ -14,15 +14,13 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { ipcRenderer } from '../../../electron-shared/electron'; import { injectable } from 'inversify'; -import { APPLICATION_STATE_CHANGE_SIGNAL } from '../../electron-common/messaging/electron-messages'; import { FrontendApplicationState, FrontendApplicationStateService } from '../../browser/frontend-application-state'; @injectable() export class ElectronFrontendApplicationStateService extends FrontendApplicationStateService { protected override doSetState(state: FrontendApplicationState): void { super.doSetState(state); - ipcRenderer.send(APPLICATION_STATE_CHANGE_SIGNAL, state); + window.electronTheiaCore.applicationStateChanged(state); } } diff --git a/packages/core/src/electron-browser/window/electron-secondary-window-service.ts b/packages/core/src/electron-browser/window/electron-secondary-window-service.ts index 987420e056fd3..fb0aed2ef831a 100644 --- a/packages/core/src/electron-browser/window/electron-secondary-window-service.ts +++ b/packages/core/src/electron-browser/window/electron-secondary-window-service.ts @@ -14,44 +14,20 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { BrowserWindow } from '../../../electron-shared/electron'; -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { injectable } from 'inversify'; import { DefaultSecondaryWindowService } from '../../browser/window/default-secondary-window-service'; @injectable() export class ElectronSecondaryWindowService extends DefaultSecondaryWindowService { - protected electronWindows: Map = new Map(); - - protected override doCreateSecondaryWindow(onClose?: (closedWin: Window) => void): Window | undefined { - const id = this.nextWindowId(); - electronRemote.getCurrentWindow().webContents.once('did-create-window', newElectronWindow => { - newElectronWindow.setMenuBarVisibility(false); - this.electronWindows.set(id, newElectronWindow); - newElectronWindow.on('closed', () => { - this.electronWindows.delete(id); - const browserWin = this.secondaryWindows.find(w => w.name === id); - if (browserWin) { - this.handleWindowClosed(browserWin, onClose); - } else { - console.warn(`Could not execute proper close handling for secondary window '${id}' because its frontend window could not be found.`); - }; - }); - }); - const win = window.open(DefaultSecondaryWindowService.SECONDARY_WINDOW_URL, id); - return win ?? undefined; + override focus(win: Window): void { + window.electronTheiaCore.focusWindow(win.name); } - override focus(win: Window): void { - // window.name is the target name given to the window.open call as the second parameter. - const electronWindow = this.electronWindows.get(win.name); - if (electronWindow) { - if (electronWindow.isMinimized()) { - electronWindow.restore(); - } - electronWindow.focus(); - } else { - console.warn(`There is no known secondary window '${win.name}'. Thus, the window could not be focussed.`); + protected override doCreateSecondaryWindow(onClose?: (closedWin: Window) => void): Window | undefined { + const w = super.doCreateSecondaryWindow(onClose); + if (w) { + window.electronTheiaCore.setMenuBarVisible(false, w.name); } + return w; } } diff --git a/packages/core/src/electron-browser/window/electron-window-service.ts b/packages/core/src/electron-browser/window/electron-window-service.ts index fd39ba39dfc33..b61a1c771914b 100644 --- a/packages/core/src/electron-browser/window/electron-window-service.ts +++ b/packages/core/src/electron-browser/window/electron-window-service.ts @@ -14,14 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { injectable, inject, postConstruct } from 'inversify'; -import * as electron from '../../../electron-shared/electron'; import { NewWindowOptions } from '../../common/window'; import { DefaultWindowService } from '../../browser/window/default-window-service'; import { ElectronMainWindowService } from '../../electron-common/electron-main-window-service'; import { ElectronWindowPreferences } from './electron-window-preferences'; -import { CloseRequestArguments, CLOSE_REQUESTED_SIGNAL, RELOAD_REQUESTED_SIGNAL, StopReason } from '../../electron-common/messaging/electron-messages'; @injectable() export class ElectronWindowService extends DefaultWindowService { @@ -62,37 +59,23 @@ export class ElectronWindowService extends DefaultWindowService { } protected override registerUnloadListeners(): void { - electron.ipcRenderer.on(CLOSE_REQUESTED_SIGNAL, (_event, closeRequestEvent: CloseRequestArguments) => this.handleCloseRequestedEvent(closeRequestEvent)); - window.addEventListener('unload', () => this.onUnloadEmitter.fire()); - } - - /** - * Run when ElectronMain detects a `close` event and emits a `close-requested` event. - * Should send an event to `electron.ipcRenderer` on the event's `confirmChannel` if it is safe to exit - * after running FrontendApplication `onWillStop` handlers or on the `cancelChannel` if it is not safe to exit. - */ - protected async handleCloseRequestedEvent(event: CloseRequestArguments): Promise { - const safeToClose = await this.isSafeToShutDown(event.reason); - if (safeToClose) { - console.debug(`Shutting down because of ${StopReason[event.reason]} request.`); - electron.ipcRenderer.send(event.confirmChannel); - } else { - electron.ipcRenderer.send(event.cancelChannel); - } + window.electronTheiaCore.setCloseRequestHandler(reason => this.isSafeToShutDown(reason)); + window.addEventListener('unload', () => { + this.onUnloadEmitter.fire(); + }); } /** * Updates the window zoom level based on the preference value. */ - protected updateWindowZoomLevel(): void { + protected async updateWindowZoomLevel(): Promise { const preferredZoomLevel = this.electronWindowPreferences['window.zoomLevel']; - const webContents = electronRemote.getCurrentWindow().webContents; - if (webContents.getZoomLevel() !== preferredZoomLevel) { - webContents.setZoomLevel(preferredZoomLevel); + if (await window.electronTheiaCore.getZoomLevel() !== preferredZoomLevel) { + window.electronTheiaCore.setZoomLevel(preferredZoomLevel); } } override reload(): void { - electron.ipcRenderer.send(RELOAD_REQUESTED_SIGNAL); + window.electronTheiaCore.requestReload(); } } diff --git a/packages/core/src/electron-common/electron-api.ts b/packages/core/src/electron-common/electron-api.ts new file mode 100644 index 0000000000000..72bc9b46b4096 --- /dev/null +++ b/packages/core/src/electron-common/electron-api.ts @@ -0,0 +1,131 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 { NativeKeyboardLayout } from '../common/keyboard/keyboard-layout-provider'; +import { Disposable } from '../common'; +import { FrontendApplicationState, StopReason } from '../common/frontend-application-state'; + +export type MenuRole = ('undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'selectAll' | 'about' | 'services' | 'hide' | 'hideOthers' | 'unhide' | 'quit'); + +export interface MenuDto { + id?: string, + label?: string, + submenu?: MenuDto[], + type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'); + checked?: boolean, + enabled?: boolean, + visible?: boolean; + role?: MenuRole; + accelerator?: string, + execute?: () => void +} + +export type InternalMenuDto = Omit & { + submenu?: InternalMenuDto[], + handlerId?: number +}; + +export type WindowEvent = 'maximize' | 'unmaximize' | 'focus'; + +export interface TheiaCoreAPI { + getSecurityToken: () => Promise; + attachSecurityToken: (endpoint: string) => Promise; + + setMenuBarVisible(visible: boolean, windowName?: string): void; + setMenu(menu: MenuDto[] | undefined): void; + + popup(menu: MenuDto[], x: number, y: number, onClosed: () => void): Promise; + closePopup(handle: number): void; + + focusWindow(name: string): void; + + showItemInFolder(fsPath: string): void; + + getTitleBarStyleAtStartup(): Promise; + setTitleBarStyle(style: string): void; + minimize(): void; + maximize(): void; + unMaximize(): void; + close(): void; + onWindowEvent(event: WindowEvent, handler: () => void): Disposable; + setCloseRequestHandler(handler: (reason: StopReason) => Promise): void; + + toggleDevTools(): void; + getZoomLevel(): Promise; + setZoomLevel(desired: number): void; + + isFullScreenable(): boolean; // TODO: this should really be async, since it blocks the renderer process + isFullScreen(): boolean; // TODO: this should really be async, since it blocks the renderer process + toggleFullScreen(): void; + + requestReload(): void; + restart(): void; + + applicationStateChanged(state: FrontendApplicationState): void; + + readClipboard(): string; + writeClipboard(text: string): void; + + onKeyboardLayoutChanged(handler: (newLayout: NativeKeyboardLayout) => void): Disposable; + + sendData(data: Uint8Array): void; + onData(handler: (data: Uint8Array) => void): Disposable; +} + +declare global { + interface Window { + electronTheiaCore: TheiaCoreAPI + } +} + +export const CHANNEL_SET_MENU = 'SetMenu'; +export const CHANNEL_SET_MENU_BAR_VISIBLE = 'SetMenuBarVisible'; +export const CHANNEL_INVOKE_MENU = 'InvokeMenu'; +export const CHANNEL_OPEN_POPUP = 'OpenPopup'; +export const CHANNEL_ON_CLOSE_POPUP = 'OnClosePopup'; +export const CHANNEL_CLOSE_POPUP = 'ClosePopup'; +export const CHANNEL_GET_SECURITY_TOKEN = 'GetSecurityToken'; +export const CHANNEL_FOCUS_WINDOW = 'FocusWindow'; +export const CHANNEL_SHOW_OPEN = 'ShowOpenDialog'; +export const CHANNEL_SHOW_SAVE = 'ShowSaveDialog'; +export const CHANNEL_SHOW_ITEM_IN_FOLDER = 'ShowItemInFolder'; +export const CHANNEL_ATTACH_SECURITY_TOKEN = 'AttachSecurityToken'; + +export const CHANNEL_GET_TITLE_STYLE_AT_STARTUP = 'GetTitleStyleAtAtStartup'; +export const CHANNEL_SET_TITLE_STYLE = 'SetTitleStyle'; +export const CHANNEL_CLOSE = 'Close'; +export const CHANNEL_MINIMIZE = 'Minimize'; +export const CHANNEL_MAXIMIZE = 'Maximize'; +export const CHANNEL_UNMAXIMIZE = 'UnMaximize'; +export const CHANNEL_ON_WINDOW_EVENT = 'OnWindowEvent'; +export const CHANNEL_TOGGLE_DEVTOOLS = 'ToggleDevtools'; +export const CHANNEL_GET_ZOOM_LEVEL = 'GetZoomLevel'; +export const CHANNEL_SET_ZOOM_LEVEL = 'SetZoomLevel'; +export const CHANNEL_IS_FULL_SCREENABLE = 'IsFullScreenable'; +export const CHANNEL_IS_FULL_SCREEN = 'IsFullScreen'; +export const CHANNEL_TOGGLE_FULL_SCREEN = 'ToggleFullScreen'; + +export const CHANNEL_REQUEST_CLOSE = 'RequestClose'; +export const CHANNEL_REQUEST_RELOAD = 'RequestReload'; +export const CHANNEL_RESTART = 'Restart'; + +export const CHANNEL_APP_STATE_CHANGED = 'ApplicationStateChanged'; + +export const CHANNEL_READ_CLIPBOARD = 'ReadClipboard'; +export const CHANNEL_WRITE_CLIPBOARD = 'WriteClipboard'; + +export const CHANNEL_KEYBOARD_LAYOUT_CHANGED = 'KeyboardLayoutChanged'; +export const CHANNEL_IPC_CONNECTION = 'IpcConnection'; diff --git a/packages/core/src/electron-common/messaging/electron-connection-handler.ts b/packages/core/src/electron-common/messaging/electron-connection-handler.ts index a7fe3e9c92b81..f42791b93bf50 100644 --- a/packages/core/src/electron-common/messaging/electron-connection-handler.ts +++ b/packages/core/src/electron-common/messaging/electron-connection-handler.ts @@ -14,17 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { ConnectionHandler } from '../../common/messaging/handler'; - -/** - * Name of the channel used with `ipcMain.on/emit`. - */ -export const THEIA_ELECTRON_IPC_CHANNEL_NAME = 'theia-electron-ipc'; - /** * 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 { -} diff --git a/packages/core/src/electron-common/messaging/electron-messages.ts b/packages/core/src/electron-common/messaging/electron-messages.ts deleted file mode 100644 index 53ec84e7edb6d..0000000000000 --- a/packages/core/src/electron-common/messaging/electron-messages.ts +++ /dev/null @@ -1,42 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2021 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 { StopReason } from '../../common/frontend-application-state'; -/** @deprecated @since 1.28 import from common/frontend-application-state instead */ -export { StopReason }; - -export const RequestTitleBarStyle = 'requestTitleBarStyle'; -export const TitleBarStyleChanged = 'titleBarStyleChanged'; -export const TitleBarStyleAtStartup = 'titleBarStyleAtStartup'; -export const Restart = 'restart'; -/** - * Emitted by main when close requested. - */ -export const CLOSE_REQUESTED_SIGNAL = 'close-requested'; -/** - * Emitted by window when a reload is requested. - */ -export const RELOAD_REQUESTED_SIGNAL = 'reload-requested'; -/** - * Emitted by the window when the application changes state - */ -export const APPLICATION_STATE_CHANGE_SIGNAL = 'application-state-changed'; - -export interface CloseRequestArguments { - confirmChannel: string; - cancelChannel: string; - reason: StopReason; -} diff --git a/packages/core/src/electron-main/electron-api-main.ts b/packages/core/src/electron-main/electron-api-main.ts new file mode 100644 index 0000000000000..e41a9b4ce3e60 --- /dev/null +++ b/packages/core/src/electron-main/electron-api-main.ts @@ -0,0 +1,286 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 { + ipcMain, BrowserWindow, Menu, MenuItemConstructorOptions, webContents, WebContents, session, shell, clipboard, IpcMainEvent +} from '@theia/electron/shared/electron'; +import * as nativeKeymap from '@theia/electron/shared/native-keymap'; + +import { inject, injectable } from 'inversify'; +import { FrontendApplicationState, StopReason } from '../common/frontend-application-state'; +import { ElectronSecurityToken } from '../electron-common/electron-token'; +import { + CHANNEL_GET_SECURITY_TOKEN, CHANNEL_SET_MENU, MenuDto, CHANNEL_INVOKE_MENU, CHANNEL_FOCUS_WINDOW, + CHANNEL_ATTACH_SECURITY_TOKEN, CHANNEL_OPEN_POPUP, CHANNEL_ON_CLOSE_POPUP, CHANNEL_CLOSE_POPUP, + CHANNEL_GET_TITLE_STYLE_AT_STARTUP, + CHANNEL_MINIMIZE, + CHANNEL_MAXIMIZE, + CHANNEL_UNMAXIMIZE, + CHANNEL_CLOSE, + CHANNEL_ON_WINDOW_EVENT, + WindowEvent, + CHANNEL_TOGGLE_DEVTOOLS, + CHANNEL_SET_ZOOM_LEVEL, + CHANNEL_GET_ZOOM_LEVEL, + CHANNEL_IS_FULL_SCREENABLE, + CHANNEL_REQUEST_CLOSE, + CHANNEL_RESTART, + CHANNEL_SET_TITLE_STYLE, + CHANNEL_REQUEST_RELOAD, + CHANNEL_APP_STATE_CHANGED, + CHANNEL_SHOW_ITEM_IN_FOLDER, + CHANNEL_READ_CLIPBOARD, + CHANNEL_WRITE_CLIPBOARD, + CHANNEL_IPC_CONNECTION, + CHANNEL_IS_FULL_SCREEN, + InternalMenuDto, + CHANNEL_SET_MENU_BAR_VISIBLE, + CHANNEL_TOGGLE_FULL_SCREEN +} + from '../electron-common/electron-api'; +import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application'; +import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common'; +import { createDisposableListener } from './event-utils'; + +@injectable() +export class TheiaMainApi implements ElectronMainApplicationContribution { + @inject(ElectronSecurityToken) + protected electronSecurityToken: ElectronSecurityToken; + + protected readonly openPopups = new Map(); + + onStart(application: ElectronMainApplication): MaybePromise { + // electron security token + ipcMain.handle(CHANNEL_GET_SECURITY_TOKEN, () => this.electronSecurityToken.value); + + ipcMain.handle(CHANNEL_ATTACH_SECURITY_TOKEN, (event, endpoint) => session.defaultSession.cookies.set({ + url: endpoint, + name: ElectronSecurityToken, + value: JSON.stringify(this.electronSecurityToken), + httpOnly: true + })); + + // application menu + ipcMain.on(CHANNEL_SET_MENU, (event, menuId: number, menu: MenuDto[]) => { + let electronMenu: Menu | null; + if (menu) { + electronMenu = Menu.buildFromTemplate(this.fromMenuDto(event.sender, menuId, menu)); + } else { + // eslint-disable-next-line no-null/no-null + electronMenu = null; + } + if (isOSX) { + Menu.setApplicationMenu(electronMenu); + } else { + BrowserWindow.fromWebContents(event.sender)?.setMenu(electronMenu); + } + }); + + ipcMain.on(CHANNEL_SET_MENU_BAR_VISIBLE, (event, visible: boolean, windowName: string | undefined) => { + let electronWindow; + if (windowName) { + electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName); + } else { + electronWindow = BrowserWindow.fromWebContents(event.sender); + } + if (electronWindow) { + electronWindow.setMenuBarVisibility(visible); + } else { + console.warn(`There is no known secondary window '${windowName}'. Thus, the menu bar could not be made visible.`); + } + }); + + // popup menu + ipcMain.handle(CHANNEL_OPEN_POPUP, (event, menuId, menu, x, y) => { + const zoom = event.sender.getZoomFactor(); + // TODO: Remove the offset once Electron fixes https://github.com/electron/electron/issues/31641 + const offset = process.platform === 'win32' ? 0 : 2; + // x and y values must be Ints or else there is a conversion error + x = Math.round(x * zoom) + offset; + y = Math.round(y * zoom) + offset; + const popup = Menu.buildFromTemplate(this.fromMenuDto(event.sender, menuId, menu)); + this.openPopups.set(menuId, popup); + popup.popup({ + callback: () => { + this.openPopups.delete(menuId); + event.sender.send(CHANNEL_ON_CLOSE_POPUP, menuId); + } + }); + }); + + ipcMain.handle(CHANNEL_CLOSE_POPUP, (event, handle) => { + if (this.openPopups.has(handle)) { + this.openPopups.get(handle)!.closePopup(); + } + }); + + // focus windows for secondary window support + ipcMain.on(CHANNEL_FOCUS_WINDOW, (event, windowName) => { + const electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName); + if (electronWindow) { + if (electronWindow.isMinimized()) { + electronWindow.restore(); + } + electronWindow.focus(); + } else { + console.warn(`There is no known secondary window '${windowName}'. Thus, the window could not be focussed.`); + } + }); + + ipcMain.on(CHANNEL_SHOW_ITEM_IN_FOLDER, (event, fsPath) => { + shell.showItemInFolder(fsPath); + }); + + ipcMain.handle(CHANNEL_GET_TITLE_STYLE_AT_STARTUP, event => application.getTitleBarStyleAtStartup(event.sender)); + + ipcMain.on(CHANNEL_SET_TITLE_STYLE, (event, style) => application.setTitleBarStyle(event.sender, style)); + + ipcMain.on(CHANNEL_MINIMIZE, event => { + BrowserWindow.fromWebContents(event.sender)?.minimize(); + }); + + ipcMain.on(CHANNEL_MAXIMIZE, event => { + BrowserWindow.fromWebContents(event.sender)?.maximize(); + }); + + ipcMain.on(CHANNEL_UNMAXIMIZE, event => { + BrowserWindow.fromWebContents(event.sender)?.unmaximize(); + }); + + ipcMain.on(CHANNEL_CLOSE, event => { + BrowserWindow.fromWebContents(event.sender)?.close(); + }); + + ipcMain.on(CHANNEL_RESTART, event => { + application.restart(event.sender); + }); + + ipcMain.on(CHANNEL_TOGGLE_DEVTOOLS, event => { + event.sender.toggleDevTools(); + }); + + ipcMain.on(CHANNEL_SET_ZOOM_LEVEL, (event, zoomLevel: number) => { + event.sender.setZoomLevel(zoomLevel); + }); + + ipcMain.handle(CHANNEL_GET_ZOOM_LEVEL, event => event.sender.getZoomLevel()); + + ipcMain.on(CHANNEL_TOGGLE_FULL_SCREEN, event => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + win.setFullScreen(!win.isFullScreen()); + } + }); + ipcMain.on(CHANNEL_IS_FULL_SCREENABLE, event => { + event.returnValue = BrowserWindow.fromWebContents(event.sender)?.isFullScreenable(); + }); + + ipcMain.on(CHANNEL_IS_FULL_SCREEN, event => { + event.returnValue = BrowserWindow.fromWebContents(event.sender)?.isFullScreen(); + }); + + ipcMain.on(CHANNEL_READ_CLIPBOARD, event => { + event.returnValue = clipboard.readText(); + }); + ipcMain.on(CHANNEL_WRITE_CLIPBOARD, (event, text) => { + clipboard.writeText(text); + }); + + nativeKeymap.onDidChangeKeyboardLayout(() => { + const newLayout = { + info: nativeKeymap.getCurrentKeyboardLayout(), + mapping: nativeKeymap.getKeyMap() + }; + for (const webContent of webContents.getAllWebContents()) { + webContent.send('keyboardLayoutChanged', newLayout); + } + }); + } + + fromMenuDto(sender: WebContents, menuId: number, menuDto: InternalMenuDto[]): MenuItemConstructorOptions[] { + return menuDto.map(dto => { + + const result: MenuItemConstructorOptions = { + id: dto.id, + label: dto.label, + type: dto.type, + checked: dto.checked, + enabled: dto.enabled, + visible: dto.visible, + role: dto.role, + accelerator: dto.accelerator + }; + if (dto.submenu) { + result.submenu = this.fromMenuDto(sender, menuId, dto.submenu); + } + if (dto.handlerId) { + result.click = () => { + sender.send(CHANNEL_INVOKE_MENU, menuId, dto.handlerId); + }; + } + return result; + }); + } +} + +let nextReplyChannel: number = 0; + +export namespace TheiaRendererAPI { + export function sendWindowEvent(wc: WebContents, event: WindowEvent): void { + wc.send(CHANNEL_ON_WINDOW_EVENT, event); + } + + export function requestClose(wc: WebContents, stopReason: StopReason): Promise { + const channelNr = nextReplyChannel++; + const confirmChannel = `confirm-${channelNr}`; + const cancelChannel = `confirm-${channelNr}`; + const disposables = new DisposableCollection(); + + return new Promise(resolve => { + wc.send(CHANNEL_REQUEST_CLOSE, stopReason, confirmChannel, cancelChannel); + createDisposableListener(ipcMain, confirmChannel, e => { + resolve(true); + }, disposables); + createDisposableListener(ipcMain, cancelChannel, e => { + resolve(false); + }, disposables); + }).finally(() => disposables.dispose()); + } + + export function onRequestReload(wc: WebContents, handler: () => void): Disposable { + return createWindowListener(wc, CHANNEL_REQUEST_RELOAD, handler); + } + + export function onApplicationStateChanged(wc: WebContents, handler: (state: FrontendApplicationState) => void): Disposable { + return createWindowListener(wc, CHANNEL_APP_STATE_CHANGED, (event, state) => handler(state as FrontendApplicationState)); + } + + export function onIpcData(handler: (sender: WebContents, data: Uint8Array) => void): Disposable { + return createDisposableListener(ipcMain, CHANNEL_IPC_CONNECTION, (event, data) => handler(event.sender, data as Uint8Array)); + } + + export function sendData(wc: WebContents, data: Uint8Array): void { + wc.send(CHANNEL_IPC_CONNECTION, data); + } + + function createWindowListener(wc: WebContents, channel: string, handler: (...args: unknown[]) => unknown): Disposable { + return createDisposableListener(ipcMain, channel, event => { + if (wc.id === event.sender.id) { + handler(); + } + }); + } +} 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 1b8a52a7bb7f8..c41d640dba26c 100644 --- a/packages/core/src/electron-main/electron-main-application-module.ts +++ b/packages/core/src/electron-main/electron-main-application-module.ts @@ -27,7 +27,7 @@ import { ElectronMessagingService } from './messaging/electron-messaging-service import { ElectronConnectionHandler } from '../electron-common/messaging/electron-connection-handler'; import { ElectronSecurityTokenService } from './electron-security-token-service'; import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory, WindowApplicationConfig } from './theia-electron-window'; -import { ElectronNativeKeymap } from './electron-native-keymap'; +import { TheiaMainApi } from './electron-api-main'; const electronSecurityToken: ElectronSecurityToken = { value: v4() }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -44,6 +44,7 @@ export default new ContainerModule(bind => { bindContributionProvider(bind, ElectronMainApplicationContribution); bind(ElectronMainApplicationContribution).toService(ElectronMessagingContribution); + bind(ElectronMainApplicationContribution).to(TheiaMainApi).inSingletonScope(); bind(ElectronMainWindowService).to(ElectronMainWindowServiceImpl).inSingletonScope(); bind(ElectronConnectionHandler).toDynamicValue(context => @@ -60,7 +61,4 @@ export default new ContainerModule(bind => { child.bind(WindowApplicationConfig).toConstantValue(config); return child.get(TheiaElectronWindow); }); - - bind(ElectronNativeKeymap).toSelf().inSingletonScope(); - bind(ElectronMainApplicationContribution).toService(ElectronNativeKeymap); }); diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index 1a4d6d560fb92..b261a904ab228 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -15,8 +15,7 @@ // ***************************************************************************** import { inject, injectable, named } from 'inversify'; -import * as electronRemoteMain from '../../electron-shared/@electron/remote/main'; -import { screen, ipcMain, app, BrowserWindow, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage } from '../../electron-shared/electron'; +import { screen, app, BrowserWindow, WebContents, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage } from '../../electron-shared/electron'; import * as path from 'path'; import { Argv } from 'yargs'; import { AddressInfo } from 'net'; @@ -32,16 +31,12 @@ 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 { - RequestTitleBarStyle, - Restart, StopReason, - TitleBarStyleAtStartup, - TitleBarStyleChanged -} from '../electron-common/messaging/electron-messages'; 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 { TheiaRendererAPI } from './electron-api-main'; +import { StopReason } from '../common/frontend-application-state'; export { ElectronMainApplicationGlobals }; @@ -156,7 +151,6 @@ export namespace ElectronMainProcessArgv { @injectable() export class ElectronMainApplication { - @inject(ContributionProvider) @named(ElectronMainApplicationContribution) protected readonly contributions: ContributionProvider; @@ -229,6 +223,24 @@ export class ElectronMainApplication { return isWindows ? 'custom' : 'native'; } + public setTitleBarStyle(webContents: WebContents, style: string): void { + this.useNativeWindowFrame = isOSX || style === 'native'; + const browserWindow = BrowserWindow.fromWebContents(webContents); + if (browserWindow) { + this.saveWindowState(browserWindow); + } else { + console.warn(`no BrowserWindow with id: ${webContents.id}`); + } + } + + /** + * @param id the id of the WebContents of the BrowserWindow in question + * @returns 'native' or 'custom' + */ + getTitleBarStyleAtStartup(webContents: WebContents): 'native' | 'custom' { + return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom'; + } + protected async launch(params: ElectronMainExecutionParams): Promise { createYargs(params.argv, params.cwd) .command('$0 [file]', false, @@ -247,11 +259,13 @@ export class ElectronMainApplication { let options = await asyncOptions; options = this.avoidOverlap(options); const electronWindow = this.windowFactory(options, this.config); - const { window: { id } } = electronWindow; + const id = electronWindow.window.webContents.id; this.windows.set(id, electronWindow); electronWindow.onDidClose(() => this.windows.delete(id)); + electronWindow.window.on('maximize', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'maximize')); + electronWindow.window.on('unmaximize', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'unmaximize')); + electronWindow.window.on('focus', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus')); this.attachSaveWindowState(electronWindow.window); - electronRemoteMain.enable(electronWindow.window.webContents); this.configureNativeSecondaryWindowCreation(electronWindow.window); return electronWindow.window; } @@ -292,12 +306,14 @@ export class ElectronMainApplication { minHeight: 120, webPreferences: { // `global` is undefined when `true`. - contextIsolation: false, + contextIsolation: true, + sandbox: false, // https://github.com/eclipse-theia/theia/issues/2018 - nodeIntegration: true, + nodeIntegration: false, // Setting the following option to `true` causes some features to break, somehow. // Issue: https://github.com/eclipse-theia/theia/issues/8577 nodeIntegrationInWorker: false, + preload: path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib/preload.js').toString() }, ...this.config.electron?.windowOptions || {}, }; @@ -418,8 +434,8 @@ export class ElectronMainApplication { }, windowStateListeners); createDisposableListener(electronWindow, 'resize', saveWindowStateDelayed, windowStateListeners); createDisposableListener(electronWindow, 'move', saveWindowStateDelayed, windowStateListeners); - windowStateListeners.push(Disposable.create(() => { try { this.didUseNativeWindowFrameOnStart.delete(electronWindow.id); } catch { } })); - this.didUseNativeWindowFrameOnStart.set(electronWindow.id, this.useNativeWindowFrame); + windowStateListeners.push(Disposable.create(() => { try { this.didUseNativeWindowFrameOnStart.delete(electronWindow.webContents.id); } catch { } })); + this.didUseNativeWindowFrameOnStart.set(electronWindow.webContents.id, this.useNativeWindowFrame); electronWindow.once('closed', () => windowStateListeners.dispose()); } @@ -532,24 +548,6 @@ export class ElectronMainApplication { app.on('will-quit', this.onWillQuit.bind(this)); app.on('second-instance', this.onSecondInstance.bind(this)); app.on('window-all-closed', this.onWindowAllClosed.bind(this)); - - ipcMain.on(TitleBarStyleChanged, ({ sender }, titleBarStyle: string) => { - this.useNativeWindowFrame = isOSX || titleBarStyle === 'native'; - const browserWindow = BrowserWindow.fromId(sender.id); - if (browserWindow) { - this.saveWindowState(browserWindow); - } else { - console.warn(`no BrowserWindow with id: ${sender.id}`); - } - }); - - ipcMain.on(Restart, ({ sender }) => { - this.restart(sender.id); - }); - - ipcMain.on(RequestTitleBarStyle, ({ sender }) => { - sender.send(TitleBarStyleAtStartup, this.didUseNativeWindowFrameOnStart.get(sender.id) ? 'native' : 'custom'); - }); } protected onWillQuit(event: ElectronEvent): void { @@ -573,10 +571,9 @@ export class ElectronMainApplication { } } - protected async restart(id: number): Promise { + public async restart(webContents: WebContents): Promise { this.restarting = true; - const window = BrowserWindow.fromId(id); - const wrapper = this.windows.get(window?.id as number); // If it's not a number, we won't get anything. + const wrapper = this.windows.get(webContents.id); if (wrapper) { const listener = wrapper.onDidClose(async () => { listener.dispose(); diff --git a/packages/core/src/electron-main/electron-native-keymap.ts b/packages/core/src/electron-main/electron-native-keymap.ts deleted file mode 100644 index 92e9323c5b628..0000000000000 --- a/packages/core/src/electron-main/electron-native-keymap.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 { webContents } from '@theia/electron/shared/electron'; -import * as nativeKeymap from '@theia/electron/shared/native-keymap'; -import { injectable } from 'inversify'; -import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application'; - -@injectable() -export class ElectronNativeKeymap implements ElectronMainApplicationContribution { - - /** - * Notify all renderer processes on keyboard layout change. - */ - onStart(application: ElectronMainApplication): void { - nativeKeymap.onDidChangeKeyboardLayout(() => { - const newLayout = { - info: nativeKeymap.getCurrentKeyboardLayout(), - mapping: nativeKeymap.getKeyMap() - }; - for (const webContent of webContents.getAllWebContents()) { - webContent.send('keyboardLayoutChanged', newLayout); - } - }); - } - -} diff --git a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts index b778598aea0e8..69f89add71132 100644 --- a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts +++ b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts @@ -14,16 +14,17 @@ // 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 { WebContents } from '@theia/electron/shared/electron'; import { inject, injectable, named, postConstruct } from 'inversify'; import { ContributionProvider } from '../../common/contribution-provider'; import { MessagingContribution } from '../../node/messaging/messaging-contribution'; -import { ElectronConnectionHandler, THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../../electron-common/messaging/electron-connection-handler'; +import { ElectronConnectionHandler } from '../../electron-common/messaging/electron-connection-handler'; import { ElectronMainApplicationContribution } from '../electron-main-application'; import { ElectronMessagingService } from './electron-messaging-service'; import { AbstractChannel, Channel, ChannelMultiplexer, MessageProvider } from '../../common/message-rpc/channel'; -import { Emitter, WriteBuffer } from '../../common'; +import { ConnectionHandler, Emitter, WriteBuffer } from '../../common'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer'; +import { TheiaRendererAPI } from '../electron-api-main'; /** * This component replicates the role filled by `MessagingContribution` but for Electron. @@ -40,7 +41,7 @@ export class ElectronMessagingContribution implements ElectronMainApplicationCon protected readonly messagingContributions: ContributionProvider; @inject(ContributionProvider) @named(ElectronConnectionHandler) - protected readonly connectionHandlers: ContributionProvider; + protected readonly connectionHandlers: ContributionProvider; protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers(); /** @@ -50,13 +51,10 @@ export class ElectronMessagingContribution implements ElectronMainApplicationCon @postConstruct() protected init(): void { - ipcMain.on(THEIA_ELECTRON_IPC_CHANNEL_NAME, (event: IpcMainEvent, data: Uint8Array) => { - this.handleIpcEvent(event, data); - }); + TheiaRendererAPI.onIpcData((sender, data) => this.handleIpcEvent(sender, data)); } - protected handleIpcEvent(event: IpcMainEvent, data: Uint8Array): void { - const sender = event.sender; + protected handleIpcEvent(sender: WebContents, data: Uint8Array): void { // Get the multiplexer for a given window id try { const windowChannelData = this.windowChannelMultiplexer.get(sender.id) ?? this.createWindowChannelData(sender); @@ -133,7 +131,7 @@ export class ElectronWebContentChannel extends AbstractChannel { writer.onCommit(buffer => { if (!this.sender.isDestroyed()) { - this.sender.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, buffer); + TheiaRendererAPI.sendData(this.sender, buffer); } }); diff --git a/packages/core/src/electron-main/theia-electron-window.ts b/packages/core/src/electron-main/theia-electron-window.ts index 9ac8e7c9a2acd..a61be0ac2b5d8 100644 --- a/packages/core/src/electron-main/theia-electron-window.ts +++ b/packages/core/src/electron-main/theia-electron-window.ts @@ -15,15 +15,15 @@ // ***************************************************************************** import { FrontendApplicationConfig } from '@theia/application-package'; -import { FrontendApplicationState } from '../common/frontend-application-state'; -import { APPLICATION_STATE_CHANGE_SIGNAL, CLOSE_REQUESTED_SIGNAL, RELOAD_REQUESTED_SIGNAL, StopReason } from '../electron-common/messaging/electron-messages'; -import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain, IpcMainEvent } from '../../electron-shared/electron'; +import { FrontendApplicationState, StopReason } from '../common/frontend-application-state'; +import { BrowserWindow, BrowserWindowConstructorOptions } from '../../electron-shared/electron'; import { inject, injectable, postConstruct } from '../../shared/inversify'; import { ElectronMainApplicationGlobals } from './electron-main-constants'; import { DisposableCollection, Emitter, Event } from '../common'; import { createDisposableListener } from './event-utils'; import { URI } from '../common/uri'; import { FileUri } from '../node/file-uri'; +import { TheiaRendererAPI } from './electron-api-main'; /** * Theia tracks the maximized state of Electron Browser Windows. @@ -138,22 +138,7 @@ export class TheiaElectronWindow { } protected checkSafeToStop(reason: StopReason): Promise { - const confirmChannel = `safe-to-close-${this._window.id}`; - const cancelChannel = `notSafeToClose-${this._window.id}`; - const temporaryDisposables = new DisposableCollection(); - return new Promise(resolve => { - this._window.webContents.send(CLOSE_REQUESTED_SIGNAL, { confirmChannel, cancelChannel, reason }); - createDisposableListener(ipcMain, confirmChannel, (e: IpcMainEvent) => { - if (this.isSender(e)) { - resolve(true); - } - }, temporaryDisposables); - createDisposableListener(ipcMain, cancelChannel, (e: IpcMainEvent) => { - if (this.isSender(e)) { - resolve(false); - } - }, temporaryDisposables); - }).finally(() => temporaryDisposables.dispose()); + return TheiaRendererAPI.requestClose(this.window.webContents, reason); } protected restoreMaximizedState(): void { @@ -165,23 +150,13 @@ export class TheiaElectronWindow { } protected trackApplicationState(): void { - createDisposableListener(ipcMain, APPLICATION_STATE_CHANGE_SIGNAL, (e: IpcMainEvent, state: FrontendApplicationState) => { - if (this.isSender(e)) { - this.applicationState = state; - } - }, this.toDispose); + this.toDispose.push(TheiaRendererAPI.onApplicationStateChanged(this.window.webContents, state => { + this.applicationState = state; + })); } protected attachReloadListener(): void { - createDisposableListener(ipcMain, RELOAD_REQUESTED_SIGNAL, (e: IpcMainEvent) => { - if (this.isSender(e)) { - this.reload(); - } - }, this.toDispose); - } - - protected isSender(e: IpcMainEvent): boolean { - return BrowserWindow.fromId(e.sender.id) === this._window; + this.toDispose.push(TheiaRendererAPI.onRequestReload(this.window.webContents, () => this.reload())); } dispose(): void { diff --git a/packages/electron/README.md b/packages/electron/README.md index 7b879f0056bdf..22374a2699c43 100644 --- a/packages/electron/README.md +++ b/packages/electron/README.md @@ -17,8 +17,6 @@ The `@theia/electron` extension bundles all Electron-specific dependencies and c ## Re-Exports - `@theia/electron/shared/...` - - `@electron/remote` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote)) - - `@electron/remote/main` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote)) - `native-keymap` (from [`native-keymap@^2.2.1`](https://www.npmjs.com/package/native-keymap)) - `electron` (from [`electron@^15.3.5`](https://www.npmjs.com/package/electron)) - `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store)) diff --git a/packages/electron/package.json b/packages/electron/package.json index c5564c761ce09..4625942bb37ea 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -3,7 +3,6 @@ "version": "1.36.0", "description": "Theia - Electron utility package", "dependencies": { - "@electron/remote": "^2.0.1 <2.0.4 || >2.0.4", "electron-store": "^8.0.0", "fix-path": "^3.0.0", "native-keymap": "^2.2.1" @@ -18,8 +17,6 @@ "theiaReExports": { "shared": { "export *": [ - "@electron/remote", - "@electron/remote/main", "native-keymap" ], "export =": [ diff --git a/packages/electron/shared/@electron/remote/index.d.ts b/packages/electron/shared/@electron/remote/index.d.ts deleted file mode 100644 index 83b7353721aca..0000000000000 --- a/packages/electron/shared/@electron/remote/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@electron/remote'; diff --git a/packages/electron/shared/@electron/remote/index.js b/packages/electron/shared/@electron/remote/index.js deleted file mode 100644 index 4097d77b477dd..0000000000000 --- a/packages/electron/shared/@electron/remote/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@electron/remote'); diff --git a/packages/electron/shared/@electron/remote/main/index.d.ts b/packages/electron/shared/@electron/remote/main/index.d.ts deleted file mode 100644 index 05ede6b8da580..0000000000000 --- a/packages/electron/shared/@electron/remote/main/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@electron/remote/main'; diff --git a/packages/electron/shared/@electron/remote/main/index.js b/packages/electron/shared/@electron/remote/main/index.js deleted file mode 100644 index 3dec179bd4de4..0000000000000 --- a/packages/electron/shared/@electron/remote/main/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@electron/remote/main'); diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json index a2d6069def5b5..0625d466e7471 100644 --- a/packages/filesystem/package.json +++ b/packages/filesystem/package.json @@ -24,6 +24,10 @@ "access": "public" }, "theiaExtensions": [ + { + "preload": "lib/electron-browser/preload", + "electronMain": "lib/electron-main/electron-main-module" + }, { "frontend": "lib/browser/filesystem-frontend-module", "backend": "lib/node/filesystem-backend-module" diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts index 3eddafb47739e..8a19cbf89f20d 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts @@ -15,8 +15,6 @@ // ***************************************************************************** import { inject, injectable } from '@theia/core/shared/inversify'; -import { FileFilter, OpenDialogOptions, SaveDialogOptions } from '@theia/core/electron-shared/electron'; -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; import URI from '@theia/core/lib/common/uri'; import { isOSX, OS } from '@theia/core/lib/common/os'; import { MaybeArray } from '@theia/core/lib/common/types'; @@ -24,12 +22,6 @@ import { MessageService } from '@theia/core/lib/common/message-service'; import { FileStat } from '../../common/files'; import { FileAccess } from '../../common/filesystem'; import { DefaultFileDialogService, OpenFileDialogProps, SaveFileDialogProps } from '../../browser/file-dialog'; - -// See https://github.com/electron/electron/blob/v9.0.2/docs/api/dialog.md -// These properties get extended with newer versions of Electron -type DialogProperties = 'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | - 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory' | 'dontAddToRecent'; - // // We are OK to use this here because the electron backend and frontend are on the same host. // If required, we can move this single service (and its module) to a dedicated Theia extension, @@ -38,6 +30,7 @@ type DialogProperties = 'openFile' | 'openDirectory' | 'multiSelections' | 'show // // eslint-disable-next-line @theia/runtime-import-check import { FileUri } from '@theia/core/lib/node/file-uri'; +import { OpenDialogOptions, SaveDialogOptions } from '../../electron-common/electron-api'; @injectable() export class ElectronFileDialogService extends DefaultFileDialogService { @@ -49,10 +42,8 @@ export class ElectronFileDialogService extends DefaultFileDialogService { override async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise | undefined> { const rootNode = await this.getRootNode(folder); if (rootNode) { - const { filePaths } = props.modal !== false ? - await electronRemote.dialog.showOpenDialog(electronRemote.getCurrentWindow(), this.toOpenDialogOptions(rootNode.uri, props)) : - await electronRemote.dialog.showOpenDialog(this.toOpenDialogOptions(rootNode.uri, props)); - if (filePaths.length === 0) { + const filePaths = await window.electronTheiaFilesystem.showOpenDialog(this.toOpenDialogOptions(rootNode.uri, props)); + if (!filePaths || filePaths.length === 0) { return undefined; } @@ -67,9 +58,8 @@ export class ElectronFileDialogService extends DefaultFileDialogService { override async showSaveDialog(props: SaveFileDialogProps, folder?: FileStat): Promise { const rootNode = await this.getRootNode(folder); if (rootNode) { - const { filePath } = props.modal !== false ? - await electronRemote.dialog.showSaveDialog(electronRemote.getCurrentWindow(), this.toSaveDialogOptions(rootNode.uri, props)) : - await electronRemote.dialog.showSaveDialog(this.toSaveDialogOptions(rootNode.uri, props)); + const filePath = await window.electronTheiaFilesystem.showSaveDialog(this.toSaveDialogOptions(rootNode.uri, props)); + if (!filePath) { return undefined; } @@ -110,120 +100,69 @@ export class ElectronFileDialogService extends DefaultFileDialogService { return unreadableResourcePaths.length === 0; } - protected toDialogOptions(uri: URI, props: SaveFileDialogProps | OpenFileDialogProps, dialogTitle: string): electron.FileDialogProps { - type Mutable = { -readonly [K in keyof T]: T[K] }; - const electronProps: Mutable = { - title: props.title || dialogTitle, - defaultPath: FileUri.fsPath(uri), - }; - const { - canSelectFiles = true, - canSelectFolders = false, - } = props as OpenFileDialogProps; - if (!isOSX && canSelectFiles && canSelectFolders) { - console.warn('canSelectFiles === true && canSelectFolders === true is only supported on OSX!'); + protected toOpenDialogOptions(uri: URI, props: OpenFileDialogProps): OpenDialogOptions { + if (!isOSX && props.canSelectFiles !== false && props.canSelectFolders === true) { + console.warn(`Cannot have 'canSelectFiles' and 'canSelectFolders' at the same time. Fallback to 'folder' dialog. \nProps was: ${JSON.stringify(props)}.`); + + // Given that both props are set, fallback to using a `folder` dialog. + props.canSelectFiles = false; + props.canSelectFolders = true; } - if ((isOSX && canSelectFiles) || !canSelectFolders) { - electronProps.filters = props.filters ? Object.entries(props.filters).map(([name, extensions]) => ({ name, extensions })) : []; - if (this.shouldAddAllFilesFilter(electronProps)) { - electronProps.filters.push({ name: 'All Files', extensions: ['*'] }); + + const result: OpenDialogOptions = { + path: FileUri.fsPath(uri) + }; + + result.title = props.title; + result.buttonLabel = props.openLabel; + result.maxWidth = props.maxWidth; + result.modal = props.modal; + result.openFiles = props.canSelectFiles; + result.openFolders = props.canSelectFolders; + result.selectMany = props.canSelectMany; + + if (props.filters) { + result.filters = []; + const filters = Object.entries(props.filters); + for (const [label, extensions] of filters) { + result.filters.push({ name: label, extensions: extensions }); } - } - return electronProps; - } - /** - * Specifies whether an _All Files_ filter should be added to the dialog. - * - * On Linux, the _All Files_ filter [hides](https://github.com/eclipse-theia/theia/issues/11321) files without an extension. - * The bug is resolved in Electron >=18. - */ - protected shouldAddAllFilesFilter(electronProps: electron.FileDialogProps): boolean { - const foundFilters = !!electronProps.filters && electronProps.filters.length > 0; - const isNotLinux = OS.type() !== OS.Type.Linux; - return isNotLinux || foundFilters; - } + // On Linux, the _All Files_ filter [hides](https://github.com/eclipse-theia/theia/issues/11321) files without an extension. + // The bug is resolved in Electron >=18. + if (props.canSelectFiles) { + if (OS.type() !== OS.Type.Linux || filters.length > 0) { + result.filters.push({ name: 'All Files', extensions: ['*'] }); + } + } + } - protected toOpenDialogOptions(uri: URI, props: OpenFileDialogProps): OpenDialogOptions { - const properties = electron.dialog.toDialogProperties(props); - const buttonLabel = props.openLabel; - return { ...this.toDialogOptions(uri, props, 'Open'), properties, buttonLabel }; + return result; } protected toSaveDialogOptions(uri: URI, props: SaveFileDialogProps): SaveDialogOptions { - const buttonLabel = props.saveLabel; if (props.inputValue) { uri = uri.resolve(props.inputValue); } - const defaultPath = FileUri.fsPath(uri); - return { ...this.toDialogOptions(uri, props, 'Save'), buttonLabel, defaultPath }; - } -} - -export namespace electron { - - /** - * Common "super" interface of the `electron.SaveDialogOptions` and `electron.OpenDialogOptions` types. - */ - export interface FileDialogProps { - - /** - * The dialog title. - */ - readonly title?: string; - - /** - * The default path, where the dialog opens. Requires an FS path. - */ - readonly defaultPath?: string; - - /** - * Resource filter. - */ - readonly filters?: FileFilter[]; + const result: SaveDialogOptions = { + path: FileUri.fsPath(uri) + }; - } + result.title = props.title; + result.buttonLabel = props.saveLabel; + result.maxWidth = props.maxWidth; + result.modal = props.modal; - export namespace dialog { - - /** - * Converts the Theia specific `OpenFileDialogProps` into an electron specific array. - * - * Note: On Windows and Linux an open dialog can not be both a file selector and a directory selector, - * so if you set properties to ['openFile', 'openDirectory'] on these platforms, a directory selector will be shown. - * - * See: https://github.com/electron/electron/issues/10252#issuecomment-322012159 - */ - export function toDialogProperties(props: OpenFileDialogProps): Array { - if (!isOSX && props.canSelectFiles !== false && props.canSelectFolders === true) { - console.warn(`Cannot have 'canSelectFiles' and 'canSelectFolders' at the same time. Fallback to 'folder' dialog. \nProps was: ${JSON.stringify(props)}.`); - - // Given that both props are set, fallback to using a `folder` dialog. - props.canSelectFiles = false; - props.canSelectFolders = true; + if (props.filters) { + result.filters = []; + const filters = Object.entries(props.filters); + for (const [label, extensions] of filters) { + result.filters.push({ name: label, extensions: extensions }); } - const properties: Array = []; - if (!isOSX) { - if (props.canSelectFiles !== false && props.canSelectFolders !== true) { - properties.push('openFile'); - } - if (props.canSelectFolders === true && props.canSelectFiles === false) { - properties.push('openDirectory'); - } - } else { - if (props.canSelectFiles !== false) { - properties.push('openFile'); - } - if (props.canSelectFolders === true) { - properties.push('openDirectory'); - properties.push('createDirectory'); - } - } - if (props.canSelectMany === true) { - properties.push('multiSelections'); - } - return properties; } + + return result; } + } diff --git a/packages/filesystem/src/electron-browser/preload.ts b/packages/filesystem/src/electron-browser/preload.ts new file mode 100644 index 0000000000000..718a33d36efbb --- /dev/null +++ b/packages/filesystem/src/electron-browser/preload.ts @@ -0,0 +1,31 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 { CHANNEL_SHOW_OPEN, CHANNEL_SHOW_SAVE, OpenDialogOptions, SaveDialogOptions, TheiaFilesystemAPI } from '../electron-common/electron-api'; + +// eslint-disable-next-line import/no-extraneous-dependencies +const { ipcRenderer, contextBridge } = require('electron'); + +const api: TheiaFilesystemAPI = { + showOpenDialog: (options: OpenDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_OPEN, options), + showSaveDialog: (options: SaveDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_SAVE, options), + +}; + +export function preload(): void { + console.log('exposing theia filesystem electron api'); + + contextBridge.exposeInMainWorld('electronTheiaFilesystem', api); +} diff --git a/packages/filesystem/src/electron-common/electron-api.ts b/packages/filesystem/src/electron-common/electron-api.ts new file mode 100644 index 0000000000000..4441d1cb38bd7 --- /dev/null +++ b/packages/filesystem/src/electron-common/electron-api.ts @@ -0,0 +1,55 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 interface FileFilter { + name: string; + extensions: string[]; +} + +export interface OpenDialogOptions { + title?: string, + maxWidth?: number, + path: string, + buttonLabel?: string, + modal?: boolean, + openFiles?: boolean, + openFolders?: boolean; + selectMany?: boolean; + filters?: FileFilter[]; +} + +export interface SaveDialogOptions { + title?: string, + maxWidth?: number, + path: string, + buttonLabel?: string, + modal?: boolean, + filters?: FileFilter[]; +} + +export interface TheiaFilesystemAPI { + showOpenDialog(options: OpenDialogOptions): Promise; + showSaveDialog(options: SaveDialogOptions): Promise; +} + +declare global { + interface Window { + electronTheiaFilesystem: TheiaFilesystemAPI + } +} + +export const CHANNEL_SHOW_OPEN = 'ShowOpenDialog'; +export const CHANNEL_SHOW_SAVE = 'ShowSaveDialog'; diff --git a/packages/filesystem/src/electron-main/electron-api-main.ts b/packages/filesystem/src/electron-main/electron-api-main.ts new file mode 100644 index 0000000000000..9a66f30ce9ef1 --- /dev/null +++ b/packages/filesystem/src/electron-main/electron-api-main.ts @@ -0,0 +1,78 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 '@theia/core/shared/inversify'; + +import { ElectronMainApplication, ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; +import { MaybePromise } from '@theia/core'; +import { CHANNEL_SHOW_OPEN, CHANNEL_SHOW_SAVE, OpenDialogOptions, SaveDialogOptions } from '../electron-common/electron-api'; +import { ipcMain, OpenDialogOptions as ElectronOpenDialogOptions, SaveDialogOptions as ElectronSaveDialogOptions, BrowserWindow, dialog } + from '@theia/core/electron-shared/electron'; + +@injectable() +export class ElectronApi implements ElectronMainApplicationContribution { + onStart(application: ElectronMainApplication): MaybePromise { + // dialogs + ipcMain.handle(CHANNEL_SHOW_OPEN, async (event, options: OpenDialogOptions) => { + const properties: ElectronOpenDialogOptions['properties'] = []; + + // checking proper combination of file/dir opening is done on the renderer side + if (options.openFiles) { + properties.push('openFile'); + } + if (options.openFolders) { + properties.push('openDirectory'); + } + + if (options.selectMany === true) { + properties.push('multiSelections'); + } + + const dialogOpts: ElectronOpenDialogOptions = { + defaultPath: options.path, + buttonLabel: options.buttonLabel, + filters: options.filters, + title: options.title, + properties: properties + }; + + if (options.modal) { + const window = BrowserWindow.fromWebContents(event.sender); + if (window) { + return (await dialog.showOpenDialog(window, dialogOpts)).filePaths; + } + } + return (await dialog.showOpenDialog(dialogOpts)).filePaths; + }); + + ipcMain.handle(CHANNEL_SHOW_SAVE, async (event, options: SaveDialogOptions) => { + const dialogOpts: ElectronSaveDialogOptions = { + defaultPath: options.path, + buttonLabel: options.buttonLabel, + filters: options.filters, + title: options.title + }; + if (options.modal && window) { + const window = BrowserWindow.fromWebContents(event.sender); + if (window) { + return (await dialog.showSaveDialog(window, dialogOpts)).filePath; + } + } + return (await dialog.showSaveDialog(dialogOpts)).filePath; + }); + + } +} diff --git a/packages/filesystem/src/electron-main/electron-main-module.ts b/packages/filesystem/src/electron-main/electron-main-module.ts new file mode 100644 index 0000000000000..175e9c47916cc --- /dev/null +++ b/packages/filesystem/src/electron-main/electron-main-module.ts @@ -0,0 +1,22 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 '@theia/core/shared/inversify'; +import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; +import { ElectronApi } from './electron-api-main'; + +export default new ContainerModule(bind => { + bind(ElectronMainApplicationContribution).to(ElectronApi).inSingletonScope(); +}); diff --git a/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts b/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts index 4fa8e0d27e170..001eb82820d10 100644 --- a/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts +++ b/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts @@ -15,11 +15,13 @@ // ***************************************************************************** import { Endpoint } from '@theia/core/lib/browser'; + import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token'; -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; import { inject, injectable } from '@theia/core/shared/inversify'; import { MiniBrowserEnvironment } from '../../browser/environment/mini-browser-environment'; +import { } from '@theia/core/lib/electron-common/electron-api'; + @injectable() export class ElectronMiniBrowserEnvironment extends MiniBrowserEnvironment { @@ -28,13 +30,8 @@ export class ElectronMiniBrowserEnvironment extends MiniBrowserEnvironment { override getEndpoint(uuid: string, hostname?: string): Endpoint { const endpoint = super.getEndpoint(uuid, hostname); - // Note: This call is async, but clients expect sync logic. - electronRemote.session.defaultSession.cookies.set({ - url: endpoint.getRestUrl().toString(true), - name: ElectronSecurityToken, - value: JSON.stringify(this.electronSecurityToken), - httpOnly: true, - }); + + window.electronTheiaCore.attachSecurityToken(endpoint.getRestUrl().toString(true)); return endpoint; } diff --git a/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts b/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts index 7c50c438a08a7..964b6163805a5 100644 --- a/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts +++ b/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts @@ -17,15 +17,14 @@ import { Command, CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService } from '@theia/core'; import { CommonCommands, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; -import * as electron from '@theia/core/electron-shared/electron'; -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; import { inject, injectable } from '@theia/core/shared/inversify'; import { FileStatNode } from '@theia/filesystem/lib/browser'; import { FileNavigatorWidget, FILE_NAVIGATOR_ID } from '../browser'; import { NavigatorContextMenu, SHELL_TABBAR_CONTEXT_REVEAL } from '../browser/navigator-contribution'; import { isWindows, isOSX } from '@theia/core/lib/common/os'; -import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; +import { } from '@theia/core/lib/electron-common/electron-api'; export const OPEN_CONTAINING_FOLDER = Command.toDefaultLocalizedCommand({ id: 'revealFileInOS', @@ -50,10 +49,7 @@ export class ElectronNavigatorMenuContribution implements MenuContribution, Comm registerCommands(commands: CommandRegistry): void { commands.registerCommand(OPEN_CONTAINING_FOLDER, UriAwareCommandHandler.MonoSelect(this.selectionService, { execute: async uri => { - // workaround for https://github.com/electron/electron/issues/4349: - // use electron.remote.shell to open the window in the foreground on Windows - const shell = isWindows ? electronRemote.shell : electron.shell; - shell.showItemInFolder(uri['codeUri'].fsPath); + window.electronTheiaCore.showItemInFolder(uri['codeUri'].fsPath); }, isEnabled: uri => !!this.workspaceService.getWorkspaceRootUri(uri), isVisible: uri => !!this.workspaceService.getWorkspaceRootUri(uri), diff --git a/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts b/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts index fd491c2439ea7..d8fe8cd80a8eb 100644 --- a/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts +++ b/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts @@ -14,12 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; -import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token'; import { WebviewWidgetFactory } from '../../browser/webview/webview-widget-factory'; import { WebviewWidgetIdentifier, WebviewWidget } from '../../browser/webview/webview'; import { CustomEditorWidgetFactory } from '../../browser/custom-editors/custom-editor-widget-factory'; import { CustomEditorWidget } from '../../browser/custom-editors/custom-editor-widget'; +import { } from '@theia/core/lib/electron-common/electron-api'; export class ElectronWebviewWidgetFactory extends WebviewWidgetFactory { @@ -34,13 +33,8 @@ export class ElectronWebviewWidgetFactory extends WebviewWidgetFactory { * * @param endpoint cookie's target url */ - protected async attachElectronSecurityCookie(endpoint: string): Promise { - await electronRemote.session.defaultSession.cookies.set({ - url: endpoint, - name: ElectronSecurityToken, - value: JSON.stringify(this.container.get(ElectronSecurityToken)), - httpOnly: true - }); + protected attachElectronSecurityCookie(endpoint: string): Promise { + return window.electronTheiaCore.attachSecurityToken(endpoint); } } @@ -59,12 +53,7 @@ export class ElectronCustomEditorWidgetFactory extends CustomEditorWidgetFactory * @param endpoint cookie's target url */ protected async attachElectronSecurityCookie(endpoint: string): Promise { - await electronRemote.session.defaultSession.cookies.set({ - url: endpoint, - name: ElectronSecurityToken, - value: JSON.stringify(this.container.get(ElectronSecurityToken)), - httpOnly: true - }); + return window.electronTheiaCore.attachSecurityToken(endpoint); } } diff --git a/yarn.lock b/yarn.lock index ea7679283aeb6..e4841e3b0f31c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -964,11 +964,6 @@ optionalDependencies: global-agent "^3.0.0" -"@electron/remote@^2.0.1 <2.0.4 || >2.0.4": - version "2.0.9" - resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-2.0.9.tgz#092ff085407bc907f45b89a72c36faa773ccf2d9" - integrity sha512-LR0W0ID6WAKHaSs0x5LX9aiG+5pFBNAJL6eQAJfGkCuZPUa6nZz+czZLdlTDETG45CgF/0raSvCtYOYUpr6c+A== - "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" From f3eda2c5ff973c9c2ac2b681a2ba85506288faed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Wed, 15 Mar 2023 16:23:30 +0100 Subject: [PATCH 2/5] Update to electron 22.3.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default behaviour of cookies in the electron session has changed and had to be adjusted accordingly constributed by STMicroelectronics Signed-off-by: Thomas Mäder --- examples/electron/package.json | 2 +- packages/core/README.md | 2 +- .../src/electron-main/electron-api-main.ts | 3 +- .../electron-security-token-service.ts | 3 +- packages/electron/README.md | 2 +- packages/electron/package.json | 2 +- yarn.lock | 224 ++---------------- 7 files changed, 33 insertions(+), 205 deletions(-) diff --git a/examples/electron/package.json b/examples/electron/package.json index 460f3fb0c1929..1f81d6076c632 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -75,6 +75,6 @@ }, "devDependencies": { "@theia/cli": "1.36.0", - "electron": "^15.3.5" + "electron": "^22.3.2" } } diff --git a/packages/core/README.md b/packages/core/README.md index d77b1d79e6457..6886554c8d803 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -70,7 +70,7 @@ export class SomeClass { - `@theia/core/electron-shared/...` - `native-keymap` (from [`native-keymap@^2.2.1`](https://www.npmjs.com/package/native-keymap)) - - `electron` (from [`electron@^15.3.5`](https://www.npmjs.com/package/electron)) + - `electron` (from [`electron@^22.3.2`](https://www.npmjs.com/package/electron)) - `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store)) - `fix-path` (from [`fix-path@^3.0.0`](https://www.npmjs.com/package/fix-path)) - `@theia/core/shared/...` diff --git a/packages/core/src/electron-main/electron-api-main.ts b/packages/core/src/electron-main/electron-api-main.ts index e41a9b4ce3e60..36c3f21e91799 100644 --- a/packages/core/src/electron-main/electron-api-main.ts +++ b/packages/core/src/electron-main/electron-api-main.ts @@ -70,7 +70,8 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { url: endpoint, name: ElectronSecurityToken, value: JSON.stringify(this.electronSecurityToken), - httpOnly: true + httpOnly: true, + sameSite: 'no_restriction' })); // application menu diff --git a/packages/core/src/electron-main/electron-security-token-service.ts b/packages/core/src/electron-main/electron-security-token-service.ts index 6daf1c050c3eb..ad4c6d2164674 100644 --- a/packages/core/src/electron-main/electron-security-token-service.ts +++ b/packages/core/src/electron-main/electron-security-token-service.ts @@ -29,7 +29,8 @@ export class ElectronSecurityTokenService { url, name: ElectronSecurityToken, value: JSON.stringify(this.electronSecurityToken), - httpOnly: true + httpOnly: true, + sameSite: 'no_restriction' }); } } diff --git a/packages/electron/README.md b/packages/electron/README.md index 22374a2699c43..f6cfe540dd056 100644 --- a/packages/electron/README.md +++ b/packages/electron/README.md @@ -18,7 +18,7 @@ The `@theia/electron` extension bundles all Electron-specific dependencies and c - `@theia/electron/shared/...` - `native-keymap` (from [`native-keymap@^2.2.1`](https://www.npmjs.com/package/native-keymap)) - - `electron` (from [`electron@^15.3.5`](https://www.npmjs.com/package/electron)) + - `electron` (from [`electron@^22.3.2`](https://www.npmjs.com/package/electron)) - `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store)) - `fix-path` (from [`fix-path@^3.0.0`](https://www.npmjs.com/package/fix-path)) diff --git a/packages/electron/package.json b/packages/electron/package.json index 4625942bb37ea..1dec7c01521b1 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -12,7 +12,7 @@ "@theia/re-exports": "1.36.0" }, "peerDependencies": { - "electron": "^15.3.5" + "electron": "^22.3.2" }, "theiaReExports": { "shared": { diff --git a/yarn.lock b/yarn.lock index e4841e3b0f31c..b3eed539f1d76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -933,22 +933,6 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@electron/get@^1.13.0": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.14.1.tgz#16ba75f02dffb74c23965e72d617adc721d27f40" - integrity sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw== - dependencies: - debug "^4.1.1" - env-paths "^2.2.0" - fs-extra "^8.1.0" - got "^9.6.0" - progress "^2.0.3" - semver "^6.2.0" - sumchecker "^3.0.1" - optionalDependencies: - global-agent "^3.0.0" - global-tunnel-ng "^2.7.1" - "@electron/get@^2.0.0": version "2.0.2" resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.2.tgz#ae2a967b22075e9c25aaf00d5941cd79c21efd7e" @@ -1639,11 +1623,6 @@ dependencies: execa "^2.0.1" -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - 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" @@ -1701,13 +1680,6 @@ resolved "https://registry.yarnpkg.com/@stroncium/procfs/-/procfs-1.2.1.tgz#6b9be6fd20fb0a4c20e99a8695e083c699bb2b45" integrity sha512-X1Iui3FUNZP18EUvysTHxt+Avu2nlVzyf90YM8OYgP6SGzTzzX/0JgObfO1AQQDzuZtNNz29bVh8h5R97JrjxA== -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - "@szmarczak/http-timer@^4.0.5": version "4.0.6" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" @@ -2064,7 +2036,7 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@16", "@types/node@>=10.0.0", "@types/node@^10.14.22", "@types/node@^14.6.2": +"@types/node@*", "@types/node@16", "@types/node@>=10.0.0", "@types/node@^10.14.22", "@types/node@^16.11.26": version "16.18.12" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.12.tgz#e3bfea80e31523fde4292a6118f19ffa24fd6f65" integrity sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw== @@ -3414,19 +3386,6 @@ cacheable-lookup@^5.0.3: resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - cacheable-request@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" @@ -3811,7 +3770,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concat-stream@^1.5.2, concat-stream@^1.6.2: +concat-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -3870,14 +3829,6 @@ config-chain@1.1.12: ini "^1.3.4" proto-list "~1.2.1" -config-chain@^1.1.11: - version "1.1.13" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" @@ -4166,7 +4117,7 @@ debounce-fn@^4.0.0: dependencies: mimic-fn "^3.0.0" -debug@2.6.9, debug@^2.6.9: +debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -4217,13 +4168,6 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== - dependencies: - mimic-response "^1.0.0" - decompress-response@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" @@ -4344,11 +4288,6 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - defer-to-connect@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" @@ -4544,11 +4483,6 @@ duplexer2@~0.1.4: dependencies: readable-stream "^2.0.2" -duplexer3@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" - integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== - duplexer@^0.1.1, duplexer@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -4626,14 +4560,14 @@ electron-window@^0.8.0: dependencies: is-electron-renderer "^2.0.0" -electron@^15.3.5: - version "15.5.7" - resolved "https://registry.yarnpkg.com/electron/-/electron-15.5.7.tgz#aadb0081c504f2c2d8f81ea5fd23e38881afe86a" - integrity sha512-n4mVlxoMc4eYx07wWFWGficL+iOMz5xZEf5dBtE/wwLm0fQpYVyW4AlknMFG9F8Css0MM0JSwNMOyRg5e1vDtg== +electron@^22.3.2: + version "22.3.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-22.3.2.tgz#6adf34561acc23938bfbe35ae771281eaf8706f2" + integrity sha512-rcE01ammPJ9RVDF3sCETyeHiDEVxV49Ywn+wXUGiG+jGtOB6erLx5jnBTf2eSVYoTXqoIbigoxGHLq4nLMLLUg== dependencies: - "@electron/get" "^1.13.0" - "@types/node" "^14.6.2" - extract-zip "^1.0.3" + "@electron/get" "^2.0.0" + "@types/node" "^16.11.26" + extract-zip "^2.0.1" emoji-regex@^8.0.0: version "8.0.0" @@ -4645,7 +4579,7 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== -encodeurl@^1.0.2, encodeurl@~1.0.2: +encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== @@ -4680,10 +4614,10 @@ engine.io-parser@~5.0.3: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== -engine.io@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.0.tgz#de27f79ecb58301171aea7956f3f6f4fa578490a" - integrity sha512-OgxY1c/RuCSeO/rTr8DIFXx76IzUUft86R7/P7MMbbkuzeqJoTNw2lmeD91IyGz41QYleIIjWeMJGgug043sfQ== +engine.io@~6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.1.tgz#8056b4526a88e779f9c280d820422d4e3eeaaae5" + integrity sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -5201,7 +5135,7 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -extract-zip@2.0.1: +extract-zip@2.0.1, extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -5212,16 +5146,6 @@ extract-zip@2.0.1: optionalDependencies: "@types/yauzl" "^2.9.1" -extract-zip@^1.0.3: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -5732,13 +5656,6 @@ get-stream@^2.2.0: object-assign "^4.0.1" pinkie-promise "^2.0.0" -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -5891,16 +5808,6 @@ global-agent@^3.0.0: semver "^7.3.2" serialize-error "^7.0.1" -global-tunnel-ng@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz#d03b5102dfde3a69914f5ee7d86761ca35d57d8f" - integrity sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg== - dependencies: - encodeurl "^1.0.2" - lodash "^4.17.10" - npm-conf "^1.1.3" - tunnel "^0.0.6" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -5968,23 +5875,6 @@ got@^11.7.0, got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - graceful-fs@4.2.10, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -6862,11 +6752,6 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== - json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -7006,13 +6891,6 @@ keytar@7.2.0: node-addon-api "^3.0.0" prebuild-install "^6.0.0" -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - keyv@^4.0.0: version "4.5.2" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.2.tgz#0e310ce73bf7851ec702f2eaf46ec4e3805cce56" @@ -7319,7 +7197,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.5.1: +lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.5.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7361,11 +7239,6 @@ loupe@^2.3.1: dependencies: get-func-name "^2.0.0" -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" @@ -7596,7 +7469,7 @@ mimic-fn@^3.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== -mimic-response@^1.0.0, mimic-response@^1.0.1: +mimic-response@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== @@ -8134,11 +8007,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-url@^4.1.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - normalize-url@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" @@ -8158,14 +8026,6 @@ npm-bundled@^2.0.0: dependencies: npm-normalize-package-bin "^2.0.0" -npm-conf@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" - integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== - dependencies: - config-chain "^1.1.11" - pify "^3.0.0" - npm-install-checks@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-5.0.0.tgz#5ff27d209a4e3542b8ac6b0c1db6063506248234" @@ -8562,11 +8422,6 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - p-cancelable@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" @@ -9093,11 +8948,6 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== - private@~0.1.5: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -9735,13 +9585,6 @@ resolve@^2.0.0-next.4: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== - dependencies: - lowercase-keys "^1.0.0" - responselike@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" @@ -10122,9 +9965,9 @@ socket.io-adapter@~2.5.2: ws "~8.11.0" socket.io-client@^4.5.3: - version "4.6.0" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.0.tgz#449255d2e0fe429f5ab47ecd3e3b1716b0039c13" - integrity sha512-2XOp18xnGghUICSd5ziUIS4rB0dhr6S8OvAps8y+HhOjFQlqGcf+FIh6fCIsKKZyWFxJeFPrZRNPGsHDTsz1Ug== + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" + integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.2" @@ -10140,14 +9983,14 @@ socket.io-parser@~4.2.1: debug "~4.3.1" socket.io@^4.5.3: - version "4.6.0" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.0.tgz#82ebfd7652572872e10dbb19533fc7cb930d0bc3" - integrity sha512-b65bp6INPk/BMMrIgVvX12x3Q+NqlGqSlTuvKQWt0BUJ3Hyy3JangBl7fEoWZTXbOKlCqNPbQ6MbWgok/km28w== + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.1.tgz#62ec117e5fce0692fa50498da9347cfb52c3bc70" + integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA== dependencies: accepts "~1.3.4" base64id "~2.0.0" debug "~4.3.2" - engine.io "~6.4.0" + engine.io "~6.4.1" socket.io-adapter "~2.5.2" socket.io-parser "~4.2.1" @@ -10738,11 +10581,6 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -10900,11 +10738,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tunnel@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" - integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -11163,13 +10996,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== - dependencies: - prepend-http "^2.0.0" - url-parse@^1.5.3: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" From e06b6aa809fde7cf9bb3a4205cc4477cc079141c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Mon, 3 Apr 2023 17:25:58 +0200 Subject: [PATCH 3/5] Addressed review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Mäder --- .../electron-main-application-module.ts | 3 ++- .../src/electron-main/electron-main-application.ts | 1 - .../file-dialog/electron-file-dialog-service.ts | 6 ++---- .../filesystem/src/electron-browser/preload.ts | 2 +- .../src/electron-main/electron-api-main.ts | 14 +++++++------- .../src/electron-main/electron-main-module.ts | 3 ++- 6 files changed, 14 insertions(+), 15 deletions(-) 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 c41d640dba26c..02751d73c336d 100644 --- a/packages/core/src/electron-main/electron-main-application-module.ts +++ b/packages/core/src/electron-main/electron-main-application-module.ts @@ -44,7 +44,8 @@ export default new ContainerModule(bind => { bindContributionProvider(bind, ElectronMainApplicationContribution); bind(ElectronMainApplicationContribution).toService(ElectronMessagingContribution); - bind(ElectronMainApplicationContribution).to(TheiaMainApi).inSingletonScope(); + bind(TheiaMainApi).toSelf().inSingletonScope(); + bind(ElectronMainApplicationContribution).toService(TheiaMainApi); bind(ElectronMainWindowService).to(ElectronMainWindowServiceImpl).inSingletonScope(); bind(ElectronConnectionHandler).toDynamicValue(context => diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index b261a904ab228..ec49244cd5ad7 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -308,7 +308,6 @@ export class ElectronMainApplication { // `global` is undefined when `true`. contextIsolation: true, sandbox: false, - // https://github.com/eclipse-theia/theia/issues/2018 nodeIntegration: false, // Setting the following option to `true` causes some features to break, somehow. // Issue: https://github.com/eclipse-theia/theia/issues/8577 diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts index 8a19cbf89f20d..2c8d06921946b 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts @@ -16,7 +16,7 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; -import { isOSX, OS } from '@theia/core/lib/common/os'; +import { isOSX } from '@theia/core/lib/common/os'; import { MaybeArray } from '@theia/core/lib/common/types'; import { MessageService } from '@theia/core/lib/common/message-service'; import { FileStat } from '../../common/files'; @@ -128,10 +128,8 @@ export class ElectronFileDialogService extends DefaultFileDialogService { result.filters.push({ name: label, extensions: extensions }); } - // On Linux, the _All Files_ filter [hides](https://github.com/eclipse-theia/theia/issues/11321) files without an extension. - // The bug is resolved in Electron >=18. if (props.canSelectFiles) { - if (OS.type() !== OS.Type.Linux || filters.length > 0) { + if (filters.length > 0) { result.filters.push({ name: 'All Files', extensions: ['*'] }); } } diff --git a/packages/filesystem/src/electron-browser/preload.ts b/packages/filesystem/src/electron-browser/preload.ts index 718a33d36efbb..e980833b526d9 100644 --- a/packages/filesystem/src/electron-browser/preload.ts +++ b/packages/filesystem/src/electron-browser/preload.ts @@ -16,7 +16,7 @@ import { CHANNEL_SHOW_OPEN, CHANNEL_SHOW_SAVE, OpenDialogOptions, SaveDialogOptions, TheiaFilesystemAPI } from '../electron-common/electron-api'; // eslint-disable-next-line import/no-extraneous-dependencies -const { ipcRenderer, contextBridge } = require('electron'); +import { ipcRenderer, contextBridge } from '@theia/core/electron-shared/electron'; const api: TheiaFilesystemAPI = { showOpenDialog: (options: OpenDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_OPEN, options), diff --git a/packages/filesystem/src/electron-main/electron-api-main.ts b/packages/filesystem/src/electron-main/electron-api-main.ts index 9a66f30ce9ef1..831cae881607b 100644 --- a/packages/filesystem/src/electron-main/electron-api-main.ts +++ b/packages/filesystem/src/electron-main/electron-api-main.ts @@ -50,9 +50,9 @@ export class ElectronApi implements ElectronMainApplicationContribution { }; if (options.modal) { - const window = BrowserWindow.fromWebContents(event.sender); - if (window) { - return (await dialog.showOpenDialog(window, dialogOpts)).filePaths; + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + return (await dialog.showOpenDialog(win, dialogOpts)).filePaths; } } return (await dialog.showOpenDialog(dialogOpts)).filePaths; @@ -65,10 +65,10 @@ export class ElectronApi implements ElectronMainApplicationContribution { filters: options.filters, title: options.title }; - if (options.modal && window) { - const window = BrowserWindow.fromWebContents(event.sender); - if (window) { - return (await dialog.showSaveDialog(window, dialogOpts)).filePath; + if (options.modal) { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + return (await dialog.showSaveDialog(win, dialogOpts)).filePath; } } return (await dialog.showSaveDialog(dialogOpts)).filePath; diff --git a/packages/filesystem/src/electron-main/electron-main-module.ts b/packages/filesystem/src/electron-main/electron-main-module.ts index 175e9c47916cc..babc337c7bd14 100644 --- a/packages/filesystem/src/electron-main/electron-main-module.ts +++ b/packages/filesystem/src/electron-main/electron-main-module.ts @@ -18,5 +18,6 @@ import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-ma import { ElectronApi } from './electron-api-main'; export default new ContainerModule(bind => { - bind(ElectronMainApplicationContribution).to(ElectronApi).inSingletonScope(); + bind(ElectronApi).toSelf().inSingletonScope(); + bind(ElectronMainApplicationContribution).toService(ElectronApi); }); From 49745c436edccb86734302235175588c0340080e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Thu, 6 Apr 2023 17:45:28 +0200 Subject: [PATCH 4/5] Addressed more PR feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Mäder --- packages/core/src/electron-browser/preload.ts | 2 +- packages/core/src/electron-common/electron-api.ts | 4 ++-- packages/core/src/electron-main/electron-api-main.ts | 11 +++++------ packages/filesystem/src/electron-browser/preload.ts | 3 +-- .../src/electron-main/electron-main-module.ts | 2 +- .../environment/electron-mini-browser-environment.ts | 2 +- .../electron-navigator-menu-contribution.ts | 2 +- .../webview/electron-webview-widget-factory.ts | 2 +- 8 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/core/src/electron-browser/preload.ts b/packages/core/src/electron-browser/preload.ts index 6df8c195e4b56..ebcdabbbfc5ed 100644 --- a/packages/core/src/electron-browser/preload.ts +++ b/packages/core/src/electron-browser/preload.ts @@ -131,7 +131,7 @@ const api: TheiaCoreAPI = { } catch (e) { console.warn('exception in close handler ', e); } - event.sender.send(confirmChannel); + event.sender.send(cancelChannel); }); }, diff --git a/packages/core/src/electron-common/electron-api.ts b/packages/core/src/electron-common/electron-api.ts index 72bc9b46b4096..31ce77a100355 100644 --- a/packages/core/src/electron-common/electron-api.ts +++ b/packages/core/src/electron-common/electron-api.ts @@ -12,7 +12,7 @@ // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 -// +// ***************************************************************************** import { NativeKeyboardLayout } from '../common/keyboard/keyboard-layout-provider'; import { Disposable } from '../common'; @@ -104,7 +104,7 @@ export const CHANNEL_SHOW_SAVE = 'ShowSaveDialog'; export const CHANNEL_SHOW_ITEM_IN_FOLDER = 'ShowItemInFolder'; export const CHANNEL_ATTACH_SECURITY_TOKEN = 'AttachSecurityToken'; -export const CHANNEL_GET_TITLE_STYLE_AT_STARTUP = 'GetTitleStyleAtAtStartup'; +export const CHANNEL_GET_TITLE_STYLE_AT_STARTUP = 'GetTitleStyleAtStartup'; export const CHANNEL_SET_TITLE_STYLE = 'SetTitleStyle'; export const CHANNEL_CLOSE = 'Close'; export const CHANNEL_MINIMIZE = 'Minimize'; diff --git a/packages/core/src/electron-main/electron-api-main.ts b/packages/core/src/electron-main/electron-api-main.ts index 36c3f21e91799..da7a6eadf8bc6 100644 --- a/packages/core/src/electron-main/electron-api-main.ts +++ b/packages/core/src/electron-main/electron-api-main.ts @@ -49,8 +49,7 @@ import { InternalMenuDto, CHANNEL_SET_MENU_BAR_VISIBLE, CHANNEL_TOGGLE_FULL_SCREEN -} - from '../electron-common/electron-api'; +} from '../electron-common/electron-api'; import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application'; import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common'; import { createDisposableListener } from './event-utils'; @@ -247,7 +246,7 @@ export namespace TheiaRendererAPI { export function requestClose(wc: WebContents, stopReason: StopReason): Promise { const channelNr = nextReplyChannel++; const confirmChannel = `confirm-${channelNr}`; - const cancelChannel = `confirm-${channelNr}`; + const cancelChannel = `cancel-${channelNr}`; const disposables = new DisposableCollection(); return new Promise(resolve => { @@ -266,7 +265,7 @@ export namespace TheiaRendererAPI { } export function onApplicationStateChanged(wc: WebContents, handler: (state: FrontendApplicationState) => void): Disposable { - return createWindowListener(wc, CHANNEL_APP_STATE_CHANGED, (event, state) => handler(state as FrontendApplicationState)); + return createWindowListener(wc, CHANNEL_APP_STATE_CHANGED, state => handler(state as FrontendApplicationState)); } export function onIpcData(handler: (sender: WebContents, data: Uint8Array) => void): Disposable { @@ -278,9 +277,9 @@ export namespace TheiaRendererAPI { } function createWindowListener(wc: WebContents, channel: string, handler: (...args: unknown[]) => unknown): Disposable { - return createDisposableListener(ipcMain, channel, event => { + return createDisposableListener(ipcMain, channel, (event, ...args) => { if (wc.id === event.sender.id) { - handler(); + handler(...args); } }); } diff --git a/packages/filesystem/src/electron-browser/preload.ts b/packages/filesystem/src/electron-browser/preload.ts index e980833b526d9..73536c6a41f1a 100644 --- a/packages/filesystem/src/electron-browser/preload.ts +++ b/packages/filesystem/src/electron-browser/preload.ts @@ -12,7 +12,7 @@ // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 -// +// ***************************************************************************** import { CHANNEL_SHOW_OPEN, CHANNEL_SHOW_SAVE, OpenDialogOptions, SaveDialogOptions, TheiaFilesystemAPI } from '../electron-common/electron-api'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -21,7 +21,6 @@ import { ipcRenderer, contextBridge } from '@theia/core/electron-shared/electron const api: TheiaFilesystemAPI = { showOpenDialog: (options: OpenDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_OPEN, options), showSaveDialog: (options: SaveDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_SAVE, options), - }; export function preload(): void { diff --git a/packages/filesystem/src/electron-main/electron-main-module.ts b/packages/filesystem/src/electron-main/electron-main-module.ts index babc337c7bd14..ffb2f4dcd2808 100644 --- a/packages/filesystem/src/electron-main/electron-main-module.ts +++ b/packages/filesystem/src/electron-main/electron-main-module.ts @@ -12,7 +12,7 @@ // 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 '@theia/core/shared/inversify'; import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; import { ElectronApi } from './electron-api-main'; diff --git a/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts b/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts index 001eb82820d10..948e29a893868 100644 --- a/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts +++ b/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts @@ -20,7 +20,7 @@ import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron- import { inject, injectable } from '@theia/core/shared/inversify'; import { MiniBrowserEnvironment } from '../../browser/environment/mini-browser-environment'; -import { } from '@theia/core/lib/electron-common/electron-api'; +import '@theia/core/lib/electron-common/electron-api'; @injectable() export class ElectronMiniBrowserEnvironment extends MiniBrowserEnvironment { diff --git a/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts b/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts index 964b6163805a5..20daeae66c231 100644 --- a/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts +++ b/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts @@ -24,7 +24,7 @@ import { NavigatorContextMenu, SHELL_TABBAR_CONTEXT_REVEAL } from '../browser/na import { isWindows, isOSX } from '@theia/core/lib/common/os'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; -import { } from '@theia/core/lib/electron-common/electron-api'; +import '@theia/core/lib/electron-common/electron-api'; export const OPEN_CONTAINING_FOLDER = Command.toDefaultLocalizedCommand({ id: 'revealFileInOS', diff --git a/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts b/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts index d8fe8cd80a8eb..0832129da2f4c 100644 --- a/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts +++ b/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts @@ -18,7 +18,7 @@ import { WebviewWidgetFactory } from '../../browser/webview/webview-widget-facto import { WebviewWidgetIdentifier, WebviewWidget } from '../../browser/webview/webview'; import { CustomEditorWidgetFactory } from '../../browser/custom-editors/custom-editor-widget-factory'; import { CustomEditorWidget } from '../../browser/custom-editors/custom-editor-widget'; -import { } from '@theia/core/lib/electron-common/electron-api'; +import '@theia/core/lib/electron-common/electron-api'; export class ElectronWebviewWidgetFactory extends WebviewWidgetFactory { From f0a6ad6b1220ebec7eb47fad748c9692ab356d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Fri, 7 Apr 2023 10:30:50 +0200 Subject: [PATCH 5/5] Fix maximize behavior on startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Mäder --- .../menu/electron-menu-contribution.ts | 16 ++++++++++------ packages/core/src/electron-browser/preload.ts | 5 ++++- .../core/src/electron-common/electron-api.ts | 3 +++ .../core/src/electron-main/electron-api-main.ts | 7 ++++++- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts index 484f9f3e88d3f..670164f3af3ab 100644 --- a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts +++ b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts @@ -232,13 +232,17 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme } protected handleWindowControls(): void { - window.electronTheiaCore.onWindowEvent('maximize', () => { - document.body.classList.add('maximized'); - }); + toggleControlButtons(); + window.electronTheiaCore.onWindowEvent('maximize', toggleControlButtons); + window.electronTheiaCore.onWindowEvent('unmaximize', toggleControlButtons); - window.electronTheiaCore.onWindowEvent('unmaximize', () => { - document.body.classList.remove('maximized'); - }); + function toggleControlButtons(): void { + if (window.electronTheiaCore.isMaximized()) { + document.body.classList.add('maximized'); + } else { + document.body.classList.remove('maximized'); + } + } } protected createControlButton(id: string, handler: () => void): HTMLElement { diff --git a/packages/core/src/electron-browser/preload.ts b/packages/core/src/electron-browser/preload.ts index ebcdabbbfc5ed..b35ea68817a6d 100644 --- a/packages/core/src/electron-browser/preload.ts +++ b/packages/core/src/electron-browser/preload.ts @@ -20,7 +20,7 @@ import { CHANNEL_ATTACH_SECURITY_TOKEN, CHANNEL_FOCUS_WINDOW, CHANNEL_GET_SECURITY_TOKEN, CHANNEL_INVOKE_MENU, CHANNEL_SET_MENU, CHANNEL_OPEN_POPUP, CHANNEL_CLOSE_POPUP, MenuDto, TheiaCoreAPI, CHANNEL_ON_CLOSE_POPUP, CHANNEL_GET_TITLE_STYLE_AT_STARTUP, WindowEvent, - CHANNEL_MINIMIZE, CHANNEL_MAXIMIZE, CHANNEL_UNMAXIMIZE, CHANNEL_CLOSE, CHANNEL_TOGGLE_DEVTOOLS, + CHANNEL_MINIMIZE, CHANNEL_IS_MAXIMIZED, CHANNEL_MAXIMIZE, CHANNEL_UNMAXIMIZE, CHANNEL_CLOSE, CHANNEL_TOGGLE_DEVTOOLS, CHANNEL_ON_WINDOW_EVENT, CHANNEL_GET_ZOOM_LEVEL, CHANNEL_SET_ZOOM_LEVEL, CHANNEL_IS_FULL_SCREENABLE, CHANNEL_TOGGLE_FULL_SCREEN, CHANNEL_IS_FULL_SCREEN, CHANNEL_SET_MENU_BAR_VISIBLE, CHANNEL_REQUEST_CLOSE, CHANNEL_SET_TITLE_STYLE, CHANNEL_RESTART, CHANNEL_REQUEST_RELOAD, CHANNEL_APP_STATE_CHANGED, CHANNEL_SHOW_ITEM_IN_FOLDER, CHANNEL_READ_CLIPBOARD, CHANNEL_WRITE_CLIPBOARD, @@ -103,6 +103,9 @@ const api: TheiaCoreAPI = { minimize: function (): void { ipcRenderer.send(CHANNEL_MINIMIZE); }, + isMaximized: function (): boolean { + return ipcRenderer.sendSync(CHANNEL_IS_MAXIMIZED); + }, maximize: function (): void { ipcRenderer.send(CHANNEL_MAXIMIZE); }, diff --git a/packages/core/src/electron-common/electron-api.ts b/packages/core/src/electron-common/electron-api.ts index 31ce77a100355..c9b6ed691f635 100644 --- a/packages/core/src/electron-common/electron-api.ts +++ b/packages/core/src/electron-common/electron-api.ts @@ -57,6 +57,7 @@ export interface TheiaCoreAPI { getTitleBarStyleAtStartup(): Promise; setTitleBarStyle(style: string): void; minimize(): void; + isMaximized(): boolean; // TODO: this should really be async, since it blocks the renderer process maximize(): void; unMaximize(): void; close(): void; @@ -109,6 +110,8 @@ export const CHANNEL_SET_TITLE_STYLE = 'SetTitleStyle'; export const CHANNEL_CLOSE = 'Close'; export const CHANNEL_MINIMIZE = 'Minimize'; export const CHANNEL_MAXIMIZE = 'Maximize'; +export const CHANNEL_IS_MAXIMIZED = 'IsMaximized'; + export const CHANNEL_UNMAXIMIZE = 'UnMaximize'; export const CHANNEL_ON_WINDOW_EVENT = 'OnWindowEvent'; export const CHANNEL_TOGGLE_DEVTOOLS = 'ToggleDevtools'; diff --git a/packages/core/src/electron-main/electron-api-main.ts b/packages/core/src/electron-main/electron-api-main.ts index da7a6eadf8bc6..e83418a864590 100644 --- a/packages/core/src/electron-main/electron-api-main.ts +++ b/packages/core/src/electron-main/electron-api-main.ts @@ -48,7 +48,8 @@ import { CHANNEL_IS_FULL_SCREEN, InternalMenuDto, CHANNEL_SET_MENU_BAR_VISIBLE, - CHANNEL_TOGGLE_FULL_SCREEN + CHANNEL_TOGGLE_FULL_SCREEN, + CHANNEL_IS_MAXIMIZED } from '../electron-common/electron-api'; import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application'; import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common'; @@ -152,6 +153,10 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { BrowserWindow.fromWebContents(event.sender)?.minimize(); }); + ipcMain.on(CHANNEL_IS_MAXIMIZED, event => { + event.returnValue = BrowserWindow.fromWebContents(event.sender)?.isMaximized(); + }); + ipcMain.on(CHANNEL_MAXIMIZE, event => { BrowserWindow.fromWebContents(event.sender)?.maximize(); });