UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

580 lines • 27.9 kB
"use strict"; // ***************************************************************************** // 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-only WITH Classpath-exception-2.0 // ***************************************************************************** var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ElectronMainApplication = exports.ElectronMainProcessArgv = exports.ElectronMainApplicationContribution = exports.ElectronMainApplicationGlobals = void 0; const inversify_1 = require("inversify"); const electron_1 = require("../../electron-shared/electron"); const path = require("path"); const fs_1 = require("fs"); const child_process_1 = require("child_process"); const application_props_1 = require("@theia/application-package/lib/application-props"); const file_uri_1 = require("../node/file-uri"); const promise_util_1 = require("../common/promise-util"); const contribution_provider_1 = require("../common/contribution-provider"); const electron_security_token_service_1 = require("./electron-security-token-service"); const electron_token_1 = require("../electron-common/electron-token"); const Storage = require("electron-store"); const common_1 = require("../common"); const window_1 = require("../common/window"); const theia_electron_window_1 = require("./theia-electron-window"); const electron_main_constants_1 = require("./electron-main-constants"); Object.defineProperty(exports, "ElectronMainApplicationGlobals", { enumerable: true, get: function () { return electron_main_constants_1.ElectronMainApplicationGlobals; } }); const event_utils_1 = require("./event-utils"); const electron_api_main_1 = require("./electron-api-main"); const frontend_application_state_1 = require("../common/frontend-application-state"); const dynamic_require_1 = require("../node/dynamic-require"); const createYargs = require('yargs/yargs'); /** * The default entrypoint will handle a very rudimentary CLI to open workspaces by doing `app path/to/workspace`. To override this behavior, you can extend and rebind the * `ElectronMainApplication` class and overriding the `launch` method. * A JSON-RPC communication between the Electron Main Process and the Renderer Processes is available: You can bind services using the `ElectronConnectionHandler` and * `ElectronIpcConnectionProvider` APIs, example: * * From an `electron-main` module: * * bind(ElectronConnectionHandler).toDynamicValue(context => * new RpcConnectionHandler(electronMainWindowServicePath, * () => context.container.get(ElectronMainWindowService)) * ).inSingletonScope(); * * And from the `electron-browser` module: * * bind(ElectronMainWindowService).toDynamicValue(context => * ElectronIpcConnectionProvider.createProxy(context.container, electronMainWindowServicePath) * ).inSingletonScope(); */ exports.ElectronMainApplicationContribution = Symbol('ElectronMainApplicationContribution'); // Extracted and modified the functionality from `yargs@15.4.0-beta.0`. // Based on https://github.com/yargs/yargs/blob/522b019c9a50924605986a1e6e0cb716d47bcbca/lib/process-argv.ts let ElectronMainProcessArgv = class ElectronMainProcessArgv { get processArgvBinIndex() { // The binary name is the first command line argument for: // - bundled Electron apps: bin argv1 argv2 ... argvn if (this.isBundledElectronApp) { return 0; } // or the second one (default) for: // - standard node apps: node bin.js argv1 argv2 ... argvn // - unbundled Electron apps: electron bin.js argv1 arg2 ... argvn return 1; } get isBundledElectronApp() { // process.defaultApp is either set by electron in an electron unbundled app, or undefined // see https://github.com/electron/electron/blob/master/docs/api/process.md#processdefaultapp-readonly return this.isElectronApp && !process.defaultApp; } get isElectronApp() { // process.versions.electron is either set by electron, or undefined // see https://github.com/electron/electron/blob/master/docs/api/process.md#processversionselectron-readonly return !!process.versions.electron; } getProcessArgvWithoutBin(argv = process.argv) { return argv.slice(this.processArgvBinIndex + 1); } getProcessArgvBin(argv = process.argv) { return argv[this.processArgvBinIndex]; } }; ElectronMainProcessArgv = __decorate([ (0, inversify_1.injectable)() ], ElectronMainProcessArgv); exports.ElectronMainProcessArgv = ElectronMainProcessArgv; let ElectronMainApplication = class ElectronMainApplication { constructor() { this.electronStore = new Storage(); this._backendPort = new promise_util_1.Deferred(); this.backendPort = this._backendPort.promise; this.useNativeWindowFrame = true; this.didUseNativeWindowFrameOnStart = new Map(); this.windows = new Map(); this.restarting = false; } get config() { if (!this._config) { throw new Error('You have to start the application first.'); } return this._config; } async start(config) { this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native'; this._config = config; this.hookApplicationEvents(); this.showInitialWindow(); const port = await this.startBackend(); this._backendPort.resolve(port); await electron_1.app.whenReady(); await this.attachElectronSecurityToken(port); await this.startContributions(); await this.launch({ secondInstance: false, argv: this.processArgv.getProcessArgvWithoutBin(process.argv), cwd: process.cwd() }); } getTitleBarStyle(config) { var _a; if (common_1.isOSX) { return 'native'; } const storedFrame = (_a = this.electronStore.get('windowstate')) === null || _a === void 0 ? void 0 : _a.frame; if (storedFrame !== undefined) { return !!storedFrame ? 'native' : 'custom'; } if (config.preferences && config.preferences['window.titleBarStyle']) { const titleBarStyle = config.preferences['window.titleBarStyle']; if (titleBarStyle === 'native' || titleBarStyle === 'custom') { return titleBarStyle; } } return common_1.isWindows ? 'custom' : 'native'; } setTitleBarStyle(webContents, style) { this.useNativeWindowFrame = common_1.isOSX || style === 'native'; this.saveState(webContents); } setBackgroundColor(webContents, backgroundColor) { this.customBackgroundColor = backgroundColor; this.saveState(webContents); } saveState(webContents) { const browserWindow = electron_1.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) { return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom'; } showInitialWindow() { if (this.config.electron.showWindowEarly) { electron_1.app.whenReady().then(async () => { const options = await this.getLastWindowOptions(); this.initialWindow = await this.createWindow({ ...options }); this.initialWindow.show(); }); } } async launch(params) { createYargs(params.argv, params.cwd) .command('$0 [file]', false, cmd => cmd .positional('file', { type: 'string' }), args => this.handleMainCommand(params, { file: args.file })).parse(); } /** * Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it. * * @param options */ async createWindow(asyncOptions = this.getDefaultTheiaWindowOptions()) { let options = await asyncOptions; options = this.avoidOverlap(options); const electronWindow = this.windowFactory(options, this.config); const id = electronWindow.window.webContents.id; this.windows.set(id, electronWindow); electronWindow.onDidClose(() => this.windows.delete(id)); electronWindow.window.on('maximize', () => electron_api_main_1.TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'maximize')); electronWindow.window.on('unmaximize', () => electron_api_main_1.TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'unmaximize')); electronWindow.window.on('focus', () => electron_api_main_1.TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus')); this.attachSaveWindowState(electronWindow.window); this.configureNativeSecondaryWindowCreation(electronWindow.window); return electronWindow.window; } async getLastWindowOptions() { const previousWindowState = this.electronStore.get('windowstate'); const windowState = (previousWindowState === null || previousWindowState === void 0 ? void 0 : previousWindowState.screenLayout) === this.getCurrentScreenLayout() ? previousWindowState : this.getDefaultTheiaWindowOptions(); return { frame: this.useNativeWindowFrame, ...this.getDefaultOptions(), ...windowState }; } avoidOverlap(options) { const existingWindowsBounds = electron_1.BrowserWindow.getAllWindows().map(window => window.getBounds()); if (existingWindowsBounds.length > 0) { while (existingWindowsBounds.some(window => window.x === options.x || window.y === options.y)) { // if the window is maximized or in fullscreen, use the default window options. if (options.isMaximized || options.isFullScreen) { options = this.getDefaultTheiaWindowOptions(); } options.x = options.x + 30; options.y = options.y + 30; } } return options; } getDefaultOptions() { var _a, _b; return { show: false, title: this.config.applicationName, backgroundColor: application_props_1.DefaultTheme.defaultBackgroundColor(((_a = this.config.electron.windowOptions) === null || _a === void 0 ? void 0 : _a.darkTheme) || electron_1.nativeTheme.shouldUseDarkColors), minWidth: 200, minHeight: 120, webPreferences: { // `global` is undefined when `true`. contextIsolation: true, sandbox: false, 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', 'frontend', 'preload.js').toString() }, ...((_b = this.config.electron) === null || _b === void 0 ? void 0 : _b.windowOptions) || {}, }; } async openDefaultWindow(params) { const options = this.getDefaultTheiaWindowOptions(); const [uri, electronWindow] = await Promise.all([this.createWindowUri(params), this.reuseOrCreateWindow(options)]); electronWindow.loadURL(uri.withFragment(window_1.DEFAULT_WINDOW_HASH).toString(true)); return electronWindow; } async openWindowWithWorkspace(workspacePath) { const options = await this.getLastWindowOptions(); const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.reuseOrCreateWindow(options)]); electronWindow.loadURL(uri.withFragment(encodeURI(workspacePath)).toString(true)); return electronWindow; } async reuseOrCreateWindow(asyncOptions) { if (!this.initialWindow) { return this.createWindow(asyncOptions); } // reset initial window after having it re-used once const window = this.initialWindow; this.initialWindow = undefined; return window; } /** Configures native window creation, i.e. using window.open or links with target "_blank" in the frontend. */ configureNativeSecondaryWindowCreation(electronWindow) { electronWindow.webContents.setWindowOpenHandler(() => { const { minWidth, minHeight } = this.getDefaultOptions(); const options = { ...this.getDefaultTheiaWindowBounds(), // We always need the native window frame for now because the secondary window does not have Theia's title bar by default. // In 'custom' title bar mode this would leave the window without any window controls (close, min, max) // TODO set to this.useNativeWindowFrame when secondary windows support a custom title bar. frame: true, minWidth, minHeight }; if (!this.useNativeWindowFrame) { // If the main window does not have a native window frame, do not show an icon in the secondary window's native title bar. // The data url is a 1x1 transparent png options.icon = electron_1.nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12P4DwQACfsD/WMmxY8AAAAASUVORK5CYII='); } return { action: 'allow', overrideBrowserWindowOptions: options, }; }); } /** * "Gently" close all windows, application will not stop if a `beforeunload` handler returns `false`. */ requestStop() { electron_1.app.quit(); } async handleMainCommand(params, options) { if (params.secondInstance === false) { await this.openWindowWithWorkspace(''); // restore previous workspace. } else if (options.file === undefined) { await this.openDefaultWindow(); } else { let workspacePath; try { workspacePath = await fs_1.promises.realpath(path.resolve(params.cwd, options.file)); } catch { console.error(`Could not resolve the workspace path. "${options.file}" is not a valid 'file' option. Falling back to the default workspace location.`); } if (workspacePath === undefined) { await this.openDefaultWindow(); } else { await this.openWindowWithWorkspace(workspacePath); } } } async createWindowUri(params = {}) { if (!('port' in params)) { params.port = (await this.backendPort).toString(); } const query = Object.entries(params).map(([name, value]) => `${name}=${value}`).join('&'); return file_uri_1.FileUri.create(this.globals.THEIA_FRONTEND_HTML_PATH) .withQuery(query); } getDefaultTheiaWindowOptions() { return { frame: this.useNativeWindowFrame, isFullScreen: false, isMaximized: false, ...this.getDefaultTheiaWindowBounds(), ...this.getDefaultOptions() }; } getDefaultTheiaWindowBounds() { // The `screen` API must be required when the application is ready. // See: https://electronjs.org/docs/api/screen#screen // We must center by hand because `browserWindow.center()` fails on multi-screen setups // See: https://github.com/electron/electron/issues/3490 const { bounds } = electron_1.screen.getDisplayNearestPoint(electron_1.screen.getCursorScreenPoint()); const height = Math.round(bounds.height * (2 / 3)); const width = Math.round(bounds.width * (2 / 3)); const y = Math.round(bounds.y + (bounds.height - height) / 2); const x = Math.round(bounds.x + (bounds.width - width) / 2); return { width, height, x, y }; } /** * Save the window geometry state on every change. */ attachSaveWindowState(electronWindow) { const windowStateListeners = new common_1.DisposableCollection(); let delayedSaveTimeout; const saveWindowStateDelayed = () => { if (delayedSaveTimeout) { clearTimeout(delayedSaveTimeout); } delayedSaveTimeout = setTimeout(() => this.saveWindowState(electronWindow), 1000); }; (0, event_utils_1.createDisposableListener)(electronWindow, 'close', () => { this.saveWindowState(electronWindow); }, windowStateListeners); (0, event_utils_1.createDisposableListener)(electronWindow, 'resize', saveWindowStateDelayed, windowStateListeners); (0, event_utils_1.createDisposableListener)(electronWindow, 'move', saveWindowStateDelayed, windowStateListeners); windowStateListeners.push(common_1.Disposable.create(() => { try { this.didUseNativeWindowFrameOnStart.delete(electronWindow.webContents.id); } catch { } })); this.didUseNativeWindowFrameOnStart.set(electronWindow.webContents.id, this.useNativeWindowFrame); electronWindow.once('closed', () => windowStateListeners.dispose()); } saveWindowState(electronWindow) { // In some circumstances the `electronWindow` can be `null` if (!electronWindow) { return; } try { const bounds = electronWindow.getBounds(); const options = { isFullScreen: electronWindow.isFullScreen(), isMaximized: electronWindow.isMaximized(), width: bounds.width, height: bounds.height, x: bounds.x, y: bounds.y, frame: this.useNativeWindowFrame, screenLayout: this.getCurrentScreenLayout(), backgroundColor: this.customBackgroundColor }; this.electronStore.set('windowstate', options); } catch (e) { console.error('Error while saving window state:', e); } } /** * Return a string unique to the current display layout. */ getCurrentScreenLayout() { return electron_1.screen.getAllDisplays().map(display => `${display.bounds.x}:${display.bounds.y}:${display.bounds.width}:${display.bounds.height}`).sort().join('-'); } /** * Start the NodeJS backend server. * * @return Running server's port promise. */ async startBackend() { // Check if we should run everything as one process. const noBackendFork = process.argv.indexOf('--no-cluster') !== -1; // We cannot use the `process.cwd()` as the application project path (the location of the `package.json` in other words) // in a bundled electron application because it depends on the way we start it. For instance, on OS X, these are a differences: // https://github.com/eclipse-theia/theia/issues/3297#issuecomment-439172274 process.env.THEIA_APP_PROJECT_PATH = this.globals.THEIA_APP_PROJECT_PATH; // Set the electron version for both the dev and the production mode. (https://github.com/eclipse-theia/theia/issues/3254) // Otherwise, the forked backend processes will not know that they're serving the electron frontend. process.env.THEIA_ELECTRON_VERSION = process.versions.electron; if (noBackendFork) { process.env[electron_token_1.ElectronSecurityToken] = JSON.stringify(this.electronSecurityToken); // The backend server main file is supposed to export a promise resolving with the port used by the http(s) server. (0, dynamic_require_1.dynamicRequire)(this.globals.THEIA_BACKEND_MAIN_PATH); // @ts-expect-error const address = await globalThis.serverAddress; return address.port; } else { const backendProcess = (0, child_process_1.fork)(this.globals.THEIA_BACKEND_MAIN_PATH, this.processArgv.getProcessArgvWithoutBin(), await this.getForkOptions()); return new Promise((resolve, reject) => { // The backend server main file is also supposed to send the resolved http(s) server port via IPC. backendProcess.on('message', (address) => { resolve(address.port); }); backendProcess.on('error', error => { reject(error); }); backendProcess.on('exit', () => { reject(new Error('backend process exited')); }); electron_1.app.on('quit', () => { // Only issue a kill signal if the backend process is running. // eslint-disable-next-line no-null/no-null if (backendProcess.exitCode === null && backendProcess.signalCode === null) { try { // If we forked the process for the clusters, we need to manually terminate it. // See: https://github.com/eclipse-theia/theia/issues/835 if (backendProcess.pid) { process.kill(backendProcess.pid); } } catch (error) { // See https://man7.org/linux/man-pages/man2/kill.2.html#ERRORS if (error.code === 'ESRCH') { return; } throw error; } } }); }); } } async getForkOptions() { return { // The backend must be a process group leader on UNIX in order to kill the tree later. // See https://nodejs.org/api/child_process.html#child_process_options_detached detached: process.platform !== 'win32', env: { ...process.env, [electron_token_1.ElectronSecurityToken]: JSON.stringify(this.electronSecurityToken), }, }; } async attachElectronSecurityToken(port) { await this.electronSecurityTokenService.setElectronSecurityTokenCookie(`http://localhost:${port}`); } hookApplicationEvents() { electron_1.app.on('will-quit', this.onWillQuit.bind(this)); electron_1.app.on('second-instance', this.onSecondInstance.bind(this)); electron_1.app.on('window-all-closed', this.onWindowAllClosed.bind(this)); } onWillQuit(event) { this.stopContributions(); } async onSecondInstance(event, argv, cwd) { const electronWindows = electron_1.BrowserWindow.getAllWindows(); if (electronWindows.length > 0) { const electronWindow = electronWindows[0]; if (electronWindow.isMinimized()) { electronWindow.restore(); } electronWindow.focus(); } } onWindowAllClosed(event) { if (!this.restarting) { this.requestStop(); } } async restart(webContents) { this.restarting = true; const wrapper = this.windows.get(webContents.id); if (wrapper) { const listener = wrapper.onDidClose(async () => { listener.dispose(); await this.launch({ secondInstance: false, argv: this.processArgv.getProcessArgvWithoutBin(process.argv), cwd: process.cwd() }); this.restarting = false; }); // If close failed or was cancelled on this occasion, don't keep listening for it. if (!await wrapper.close(frontend_application_state_1.StopReason.Restart)) { listener.dispose(); } } } async startContributions() { const promises = []; for (const contribution of this.contributions.getContributions()) { if (contribution.onStart) { promises.push(contribution.onStart(this)); } } await Promise.all(promises); } stopContributions() { for (const contribution of this.contributions.getContributions()) { if (contribution.onStop) { contribution.onStop(this); } } } }; __decorate([ (0, inversify_1.inject)(contribution_provider_1.ContributionProvider), (0, inversify_1.named)(exports.ElectronMainApplicationContribution), __metadata("design:type", Object) ], ElectronMainApplication.prototype, "contributions", void 0); __decorate([ (0, inversify_1.inject)(electron_main_constants_1.ElectronMainApplicationGlobals), __metadata("design:type", Object) ], ElectronMainApplication.prototype, "globals", void 0); __decorate([ (0, inversify_1.inject)(ElectronMainProcessArgv), __metadata("design:type", ElectronMainProcessArgv) ], ElectronMainApplication.prototype, "processArgv", void 0); __decorate([ (0, inversify_1.inject)(electron_security_token_service_1.ElectronSecurityTokenService), __metadata("design:type", electron_security_token_service_1.ElectronSecurityTokenService) ], ElectronMainApplication.prototype, "electronSecurityTokenService", void 0); __decorate([ (0, inversify_1.inject)(electron_token_1.ElectronSecurityToken), __metadata("design:type", Object) ], ElectronMainApplication.prototype, "electronSecurityToken", void 0); __decorate([ (0, inversify_1.inject)(theia_electron_window_1.TheiaElectronWindowFactory), __metadata("design:type", Function) ], ElectronMainApplication.prototype, "windowFactory", void 0); ElectronMainApplication = __decorate([ (0, inversify_1.injectable)() ], ElectronMainApplication); exports.ElectronMainApplication = ElectronMainApplication; //# sourceMappingURL=electron-main-application.js.map