@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
862 lines (776 loc) • 37.7 kB
text/typescript
// *****************************************************************************
// 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
// *****************************************************************************
import { inject, injectable, named } from 'inversify';
import {
screen, app, BrowserWindow, WebContents, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage,
nativeTheme, shell, dialog
} from '../../electron-shared/electron';
import * as path from 'path';
import { Argv } from 'yargs';
import { AddressInfo } from 'net';
import { promises as fs } from 'fs';
import { existsSync, mkdirSync } from 'fs-extra';
import { fork, ForkOptions } from 'child_process';
import { DefaultTheme, ElectronFrontendApplicationConfig, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
import URI from '../common/uri';
import { FileUri } from '../common/file-uri';
import { Deferred, timeout } from '../common/promise-util';
import { MaybePromise } from '../common/types';
import { ContributionProvider } from '../common/contribution-provider';
import { ElectronSecurityTokenService } from './electron-security-token-service';
import { ElectronSecurityToken } from '../electron-common/electron-token';
import Storage = require('electron-store');
import { CancellationTokenSource, Disposable, DisposableCollection, Path, isOSX, isWindows } from '../common';
import { DEFAULT_WINDOW_HASH, WindowSearchParams } 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';
import { dynamicRequire } from '../node/dynamic-require';
export { ElectronMainApplicationGlobals };
const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs');
/**
* Options passed to the main/default command handler.
*/
export interface ElectronMainCommandOptions {
/**
* By default, the first positional argument. Should be either a relative or absolute file-system path pointing to a file or a folder.
*/
readonly file?: string;
readonly cwd: string;
/**
* If the app is launched for the first time, `secondInstance` is false.
* If the app is already running but user relaunches it, `secondInstance` is true.
*/
readonly secondInstance: boolean;
}
/**
* 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();
*/
export const ElectronMainApplicationContribution = Symbol('ElectronMainApplicationContribution');
export interface ElectronMainApplicationContribution {
/**
* The application is ready and is starting. This is the time to initialize
* services global to this process.
*
* Invoked when the electron-main process starts for the first time.
*/
onStart?(application: ElectronMainApplication): MaybePromise<void>;
/**
* The application is stopping. Contributions must perform only synchronous operations.
*/
onStop?(application: ElectronMainApplication): void;
}
// 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
export class ElectronMainProcessArgv {
protected get processArgvBinIndex(): number {
// 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(): boolean {
// 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 as ElectronMainProcessArgv.ElectronMainProcess).defaultApp;
}
get isElectronApp(): boolean {
// 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 as ElectronMainProcessArgv.ElectronMainProcess).versions.electron;
}
getProcessArgvWithoutBin(argv = process.argv): Array<string> {
return argv.slice(this.processArgvBinIndex + 1);
}
getProcessArgvBin(argv = process.argv): string {
return argv[this.processArgvBinIndex];
}
}
export namespace ElectronMainProcessArgv {
export interface ElectronMainProcess extends NodeJS.Process {
readonly defaultApp: boolean;
readonly versions: NodeJS.ProcessVersions & {
readonly electron: string;
};
}
}
export class ElectronMainApplication {
protected readonly contributions: ContributionProvider<ElectronMainApplicationContribution>;
protected readonly globals: ElectronMainApplicationGlobals;
protected processArgv: ElectronMainProcessArgv;
protected electronSecurityTokenService: ElectronSecurityTokenService;
protected readonly electronSecurityToken: ElectronSecurityToken;
protected readonly windowFactory: TheiaElectronWindowFactory;
protected isPortable = this.makePortable();
protected readonly electronStore = new Storage<{
windowstate?: TheiaBrowserWindowOptions
}>();
protected readonly _backendPort = new Deferred<number>();
readonly backendPort = this._backendPort.promise;
protected _config: FrontendApplicationConfig | undefined;
protected useNativeWindowFrame: boolean = true;
protected customBackgroundColor?: string;
protected didUseNativeWindowFrameOnStart = new Map<number, boolean>();
protected windows = new Map<number, TheiaElectronWindow>();
protected activeWindowStack: number[] = [];
protected restarting = false;
/** Used to temporarily store the reference to an early created main window */
protected initialWindow?: BrowserWindow;
get config(): FrontendApplicationConfig {
if (!this._config) {
throw new Error('You have to start the application first.');
}
return this._config;
}
protected makePortable(): boolean {
const dataFolderPath = path.join(app.getAppPath(), 'data');
const appDataPath = path.join(dataFolderPath, 'app-data');
if (existsSync(dataFolderPath)) {
if (!existsSync(appDataPath)) {
mkdirSync(appDataPath);
}
app.setPath('userData', appDataPath);
return true;
} else {
return false;
}
}
async start(config: FrontendApplicationConfig): Promise<void> {
const argv = this.processArgv.getProcessArgvWithoutBin(process.argv);
createYargs(argv, process.cwd())
.help(false)
.command('$0 [file]', false,
cmd => cmd
.option('electronUserData', {
type: 'string',
describe: 'The area where the electron main process puts its data'
})
.positional('file', { type: 'string' }),
async args => {
if (args.electronUserData) {
console.info(`using electron user data area : '${args.electronUserData}'`);
await fs.mkdir(args.electronUserData, { recursive: true });
app.setPath('userData', args.electronUserData);
}
this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
this._config = config;
this.hookApplicationEvents();
this.showInitialWindow(argv.includes('--open-url') ? argv[argv.length - 1] : undefined);
const port = await this.startBackend();
this._backendPort.resolve(port);
await app.whenReady();
await this.attachElectronSecurityToken(port);
await this.startContributions();
this.handleMainCommand({
file: args.file,
cwd: process.cwd(),
secondInstance: false
});
},
).parse();
}
protected getTitleBarStyle(config: FrontendApplicationConfig): 'native' | 'custom' {
if ('THEIA_ELECTRON_DISABLE_NATIVE_ELEMENTS' in process.env && process.env.THEIA_ELECTRON_DISABLE_NATIVE_ELEMENTS === '1') {
return 'custom';
}
if (isOSX) {
return 'native';
}
const storedFrame = this.electronStore.get('windowstate')?.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 isWindows ? 'custom' : 'native';
}
public setTitleBarStyle(webContents: WebContents, style: string): void {
this.useNativeWindowFrame = isOSX || style === 'native';
this.saveState(webContents);
}
setBackgroundColor(webContents: WebContents, backgroundColor: string): void {
BrowserWindow.fromWebContents(webContents)?.setBackgroundColor(backgroundColor);
this.customBackgroundColor = backgroundColor;
this.saveState(webContents);
}
protected saveState(webContents: Electron.WebContents): void {
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 determineSplashScreenBounds(initialWindowBounds: { x: number, y: number, width: number, height: number }):
Promise<{ x: number, y: number, width: number, height: number }> {
const splashScreenOptions = this.getSplashScreenOptions();
const width = splashScreenOptions?.width ?? 640;
const height = splashScreenOptions?.height ?? 480;
// determine the screen on which to show the splash screen via the center of the window to show
const windowCenterPoint = { x: initialWindowBounds.x + (initialWindowBounds.width / 2), y: initialWindowBounds.y + (initialWindowBounds.height / 2) };
const { bounds } = screen.getDisplayNearestPoint(windowCenterPoint);
// place splash screen center of screen
const screenCenterPoint = { x: bounds.x + (bounds.width / 2), y: bounds.y + (bounds.height / 2) };
const x = screenCenterPoint.x - (width / 2);
const y = screenCenterPoint.y - (height / 2);
return {
x, y, width, height
};
}
protected isShowWindowEarly(): boolean {
return !!this.config.electron.showWindowEarly &&
!('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1');
}
protected showInitialWindow(urlToOpen: string | undefined): void {
if (this.isShowWindowEarly() || this.isShowSplashScreen()) {
app.whenReady().then(async () => {
const options = await this.getLastWindowOptions();
// If we want to show a splash screen, don't auto open the main window
if (this.isShowSplashScreen()) {
options.preventAutomaticShow = true;
}
this.initialWindow = await this.createWindow({ ...options });
TheiaRendererAPI.onApplicationStateChanged(this.initialWindow.webContents, state => {
if (state === 'ready' && urlToOpen) {
this.openUrl(urlToOpen);
}
});
if (this.isShowSplashScreen()) {
console.log('Showing splash screen');
this.configureAndShowSplashScreen(this.initialWindow);
}
// Show main window early if windows shall be shown early and splash screen is not configured
if (this.isShowWindowEarly() && !this.isShowSplashScreen()) {
console.log('Showing main window early');
this.initialWindow.show();
}
});
}
}
protected async configureAndShowSplashScreen(mainWindow: BrowserWindow): Promise<BrowserWindow> {
const splashScreenOptions = this.getSplashScreenOptions()!;
console.debug('SplashScreen options', splashScreenOptions);
const splashScreenBounds = await this.determineSplashScreenBounds(mainWindow.getBounds());
const splashScreenWindow = new BrowserWindow({
...splashScreenBounds,
frame: false,
alwaysOnTop: true,
show: false,
transparent: true,
webPreferences: {
backgroundThrottling: false
}
});
if (this.isShowWindowEarly()) {
console.log('Showing splash screen early');
splashScreenWindow.show();
} else {
splashScreenWindow.on('ready-to-show', () => {
splashScreenWindow.show();
});
}
splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, splashScreenOptions.content!).toString());
// close splash screen and show main window once frontend is ready or a timeout is hit
const cancelTokenSource = new CancellationTokenSource();
const minTime = timeout(splashScreenOptions.minDuration ?? 0, cancelTokenSource.token);
const maxTime = timeout(splashScreenOptions.maxDuration ?? 30000, cancelTokenSource.token);
const showWindowAndCloseSplashScreen = () => {
cancelTokenSource.cancel();
if (!mainWindow.isVisible()) {
mainWindow.show();
}
splashScreenWindow.close();
};
TheiaRendererAPI.onApplicationStateChanged(mainWindow.webContents, state => {
if (state === 'ready') {
minTime.then(() => showWindowAndCloseSplashScreen());
}
});
maxTime.then(() => showWindowAndCloseSplashScreen());
return splashScreenWindow;
}
protected isShowSplashScreen(): boolean {
return !process.env.THEIA_NO_SPLASH && typeof this.config.electron.splashScreenOptions === 'object' && !!this.config.electron.splashScreenOptions.content;
}
protected getSplashScreenOptions(): ElectronFrontendApplicationConfig.SplashScreenOptions | undefined {
if (this.isShowSplashScreen()) {
return this.config.electron.splashScreenOptions;
}
return undefined;
}
/**
* 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: MaybePromise<TheiaBrowserWindowOptions> = this.getDefaultTheiaWindowOptions()): Promise<BrowserWindow> {
let options = await asyncOptions;
options = this.avoidOverlap(options);
const electronWindow = this.windowFactory(options, this.config);
const id = electronWindow.window.webContents.id;
this.activeWindowStack.push(id);
this.windows.set(id, electronWindow);
electronWindow.onDidClose(() => {
const stackIndex = this.activeWindowStack.indexOf(id);
if (stackIndex >= 0) {
this.activeWindowStack.splice(stackIndex, 1);
}
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', () => {
const stackIndex = this.activeWindowStack.indexOf(id);
if (stackIndex >= 0) {
this.activeWindowStack.splice(stackIndex, 1);
}
this.activeWindowStack.unshift(id);
TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus');
});
this.attachSaveWindowState(electronWindow.window);
return electronWindow.window;
}
async getLastWindowOptions(): Promise<TheiaBrowserWindowOptions> {
const previousWindowState: TheiaBrowserWindowOptions | undefined = this.electronStore.get('windowstate');
const windowState = previousWindowState?.screenLayout === this.getCurrentScreenLayout()
? previousWindowState
: this.getDefaultTheiaWindowOptions();
return {
frame: this.useNativeWindowFrame,
...this.getDefaultOptions(),
...windowState
};
}
protected avoidOverlap(options: TheiaBrowserWindowOptions): TheiaBrowserWindowOptions {
const existingWindowsBounds = 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;
}
protected getDefaultOptions(): TheiaBrowserWindowOptions {
return {
show: false,
title: this.config.applicationName,
backgroundColor: DefaultTheme.defaultBackgroundColor(this.config.electron.windowOptions?.darkTheme || 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,
backgroundThrottling: false,
preload: path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib', 'frontend', 'preload.js').toString()
},
...this.config.electron?.windowOptions || {},
};
}
async openDefaultWindow(params?: WindowSearchParams): Promise<BrowserWindow> {
const options = this.getDefaultTheiaWindowOptions();
const [uri, electronWindow] = await Promise.all([this.createWindowUri(params), this.reuseOrCreateWindow(options)]);
electronWindow.loadURL(uri.withFragment(DEFAULT_WINDOW_HASH).toString(true));
return electronWindow;
}
protected async openWindowWithWorkspace(workspacePath: string): Promise<BrowserWindow> {
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;
}
protected async reuseOrCreateWindow(asyncOptions: MaybePromise<TheiaBrowserWindowOptions>): Promise<BrowserWindow> {
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;
}
/**
* "Gently" close all windows, application will not stop if a `beforeunload` handler returns `false`.
*/
requestStop(): void {
app.quit();
}
protected async handleMainCommand(options: ElectronMainCommandOptions): Promise<void> {
let workspacePath: string | undefined;
if (options.file) {
try {
workspacePath = await fs.realpath(path.resolve(options.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.openWindowWithWorkspace(workspacePath);
} else {
if (options.secondInstance === false) {
await this.openWindowWithWorkspace(''); // restore previous workspace.
} else if (options.file === undefined) {
await this.openDefaultWindow();
}
}
}
async openUrl(url: string): Promise<void> {
for (const id of this.activeWindowStack) {
const window = this.windows.get(id);
if (window && await window.openUrl(url)) {
break;
}
}
}
protected async createWindowUri(params: WindowSearchParams = {}): Promise<URI> {
if (!('port' in params)) {
params.port = (await this.backendPort).toString();
}
const query = Object.entries(params).map(([name, value]) => `${name}=${value}`).join('&');
return FileUri.create(this.globals.THEIA_FRONTEND_HTML_PATH)
.withQuery(query);
}
protected getDefaultTheiaWindowOptions(): TheiaBrowserWindowOptions {
return {
frame: this.useNativeWindowFrame,
isFullScreen: false,
isMaximized: false,
...this.getDefaultTheiaWindowBounds(),
...this.getDefaultOptions()
};
}
protected getDefaultTheiaSecondaryWindowBounds(): TheiaBrowserWindowOptions {
return {};
}
protected getDefaultTheiaWindowBounds(): TheiaBrowserWindowOptions {
// 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 } = screen.getDisplayNearestPoint(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.
*/
protected attachSaveWindowState(electronWindow: BrowserWindow): void {
const windowStateListeners = new DisposableCollection();
let delayedSaveTimeout: NodeJS.Timeout | undefined;
const saveWindowStateDelayed = () => {
if (delayedSaveTimeout) {
clearTimeout(delayedSaveTimeout);
}
delayedSaveTimeout = setTimeout(() => this.saveWindowState(electronWindow), 1000);
};
createDisposableListener(electronWindow, 'close', () => {
this.saveWindowState(electronWindow);
}, windowStateListeners);
createDisposableListener(electronWindow, 'resize', saveWindowStateDelayed, windowStateListeners);
createDisposableListener(electronWindow, 'move', saveWindowStateDelayed, windowStateListeners);
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());
}
protected saveWindowState(electronWindow: BrowserWindow): void {
// In some circumstances the `electronWindow` can be `null`
if (!electronWindow) {
return;
}
try {
const bounds = electronWindow.getBounds();
const options: TheiaBrowserWindowOptions = {
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 ?? electronWindow.getBackgroundColor()
};
this.electronStore.set('windowstate', options);
} catch (e) {
console.error('Error while saving window state:', e);
}
}
/**
* Return a string unique to the current display layout.
*/
protected getCurrentScreenLayout(): string {
return screen.getAllDisplays().map(
display => `${display.bounds.x}:${display.bounds.y}:${display.bounds.width}:${display.bounds.height}`
).sort().join('-');
}
/**
* Start the NodeJS backend server.
*
* @return Running server's port promise.
*/
protected async startBackend(): Promise<number> {
// Check if we should run everything as one process.
const noBackendFork = process.argv.indexOf('--no-cluster') !== -1;
// 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[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.
dynamicRequire(this.globals.THEIA_BACKEND_MAIN_PATH);
// @ts-expect-error
const address: AddressInfo = await globalThis.serverAddress;
return address.port;
} else {
const backendProcess = 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: AddressInfo) => {
resolve(address.port);
});
backendProcess.on('error', error => {
reject(error);
});
backendProcess.on('exit', code => {
reject(code);
});
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;
}
}
});
});
}
}
protected async getForkOptions(): Promise<ForkOptions> {
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,
[ElectronSecurityToken]: JSON.stringify(this.electronSecurityToken),
},
};
}
protected async attachElectronSecurityToken(port: number): Promise<void> {
await this.electronSecurityTokenService.setElectronSecurityTokenCookie(`http://localhost:${port}`);
}
protected hookApplicationEvents(): void {
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));
app.on('web-contents-created', this.onWebContentsCreated.bind(this));
if (isWindows) {
const args = this.processArgv.isBundledElectronApp ? [] : [app.getAppPath()];
args.push('--open-url');
app.setAsDefaultProtocolClient(this.config.electron.uriScheme, process.execPath, args);
} else {
app.on('open-url', (evt, url) => {
this.openUrl(url);
});
}
}
protected onWillQuit(event: ElectronEvent): void {
this.stopContributions();
}
protected async onSecondInstance(event: ElectronEvent, argv: string[], cwd: string): Promise<void> {
if (argv.includes('--open-url')) {
this.openUrl(argv[argv.length - 1]);
} else {
createYargs(this.processArgv.getProcessArgvWithoutBin(argv), process.cwd())
.help(false)
.command('$0 [file]', false,
cmd => cmd
.positional('file', { type: 'string' }),
async args => {
await this.handleMainCommand({
file: args.file,
cwd: process.cwd(),
secondInstance: true
});
},
).parse();
}
}
protected onWebContentsCreated(event: ElectronEvent, webContents: WebContents): void {
// Block any in-page navigation except loading the secondary window contents
webContents.on('will-navigate', evt => {
if (new URI(evt.url).path.fsPath() !== new Path(this.globals.THEIA_SECONDARY_WINDOW_HTML_PATH).fsPath()) {
evt.preventDefault();
}
});
webContents.setWindowOpenHandler(details => {
// if it's a secondary window, allow it to open
if (new URI(details.url).path.fsPath() === new Path(this.globals.THEIA_SECONDARY_WINDOW_HTML_PATH).fsPath()) {
const { minWidth, minHeight } = this.getDefaultOptions();
const options: BrowserWindowConstructorOptions = {
...this.getDefaultTheiaSecondaryWindowBounds(),
// 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 = nativeImage.createFromDataURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12P4DwQACfsD/WMmxY8AAAAASUVORK5CYII=');
}
return {
action: 'allow',
overrideBrowserWindowOptions: options,
};
} else {
const uri: URI = new URI(details.url);
let okToOpen = uri.scheme === 'https' || uri.scheme === 'http';
if (!okToOpen) {
const button = dialog.showMessageBoxSync(BrowserWindow.fromWebContents(webContents)!, {
message: `Open link\n\n${details.url}\n\nin the system handler?`,
type: 'question',
title: 'Open Link',
buttons: ['OK', 'Cancel'],
defaultId: 1,
cancelId: 1
});
okToOpen = button === 0;
}
if (okToOpen) {
shell.openExternal(details.url, {});
}
return { action: 'deny' };
}
});
}
protected onWindowAllClosed(event: ElectronEvent): void {
if (!this.restarting) {
this.requestStop();
}
}
public async restart(webContents: WebContents): Promise<void> {
this.restarting = true;
const wrapper = this.windows.get(webContents.id);
if (wrapper) {
const listener = wrapper.onDidClose(async () => {
listener.dispose();
await this.handleMainCommand({
secondInstance: false,
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(StopReason.Restart)) {
listener.dispose();
}
}
}
protected async startContributions(): Promise<void> {
const promises = [];
for (const contribution of this.contributions.getContributions()) {
if (contribution.onStart) {
promises.push(contribution.onStart(this));
}
}
await Promise.all(promises);
}
protected stopContributions(): void {
for (const contribution of this.contributions.getContributions()) {
if (contribution.onStop) {
contribution.onStop(this);
}
}
}
}