"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() {
        const options = this.getDefaultTheiaWindowOptions();
        const [uri, electronWindow] = await Promise.all([this.createWindowUri(), 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() {
        return file_uri_1.FileUri.create(this.globals.THEIA_FRONTEND_HTML_PATH)
            .withQuery(`port=${await this.backendPort}`);
    }
    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