"use strict";
// *****************************************************************************
// Copyright (C) 2017 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
// *****************************************************************************
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);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BackendApplication = exports.BackendApplicationCliContribution = exports.BackendApplicationContribution = exports.BackendApplicationServer = void 0;
const path = require("path");
const http = require("http");
const https = require("https");
const express = require("express");
const fs = require("fs-extra");
const inversify_1 = require("inversify");
const common_1 = require("../common");
const promise_util_1 = require("../common/promise-util");
const index_1 = require("../common/index");
const application_package_1 = require("@theia/application-package");
const process_utils_1 = require("./process-utils");
const APP_PROJECT_PATH = 'app-project-path';
const TIMER_WARNING_THRESHOLD = 50;
const DEFAULT_PORT = index_1.environment.electron.is() ? 0 : 3000;
const DEFAULT_HOST = 'localhost';
const DEFAULT_SSL = false;
exports.BackendApplicationServer = Symbol('BackendApplicationServer');
exports.BackendApplicationContribution = Symbol('BackendApplicationContribution');
let BackendApplicationCliContribution = class BackendApplicationCliContribution {
    configure(conf) {
        conf.option('port', { alias: 'p', description: 'The port the backend server listens on.', type: 'number', default: DEFAULT_PORT });
        conf.option('hostname', { alias: 'h', description: 'The allowed hostname for connections.', type: 'string', default: DEFAULT_HOST });
        conf.option('ssl', { description: 'Use SSL (HTTPS), cert and certkey must also be set', type: 'boolean', default: DEFAULT_SSL });
        conf.option('cert', { description: 'Path to SSL certificate.', type: 'string' });
        conf.option('certkey', { description: 'Path to SSL certificate key.', type: 'string' });
        conf.option(APP_PROJECT_PATH, { description: 'Sets the application project directory', default: this.appProjectPath() });
    }
    setArguments(args) {
        this.port = args.port;
        this.hostname = args.hostname;
        this.ssl = args.ssl;
        this.cert = args.cert;
        this.certkey = args.certkey;
        this.projectPath = args[APP_PROJECT_PATH];
    }
    appProjectPath() {
        if (index_1.environment.electron.is()) {
            if (process.env.THEIA_APP_PROJECT_PATH) {
                return process.env.THEIA_APP_PROJECT_PATH;
            }
            throw new Error('The \'THEIA_APP_PROJECT_PATH\' environment variable must be set when running in electron.');
        }
        return process.cwd();
    }
};
BackendApplicationCliContribution = __decorate([
    (0, inversify_1.injectable)()
], BackendApplicationCliContribution);
exports.BackendApplicationCliContribution = BackendApplicationCliContribution;
/**
 * The main entry point for Theia applications.
 */
let BackendApplication = class BackendApplication {
    constructor(contributionsProvider, cliParams) {
        this.contributionsProvider = contributionsProvider;
        this.cliParams = cliParams;
        this.app = express();
        process.on('uncaughtException', error => {
            this.handleUncaughtError(error);
        });
        // Workaround for Electron not installing a handler to ignore SIGPIPE error
        // (https://github.com/electron/electron/issues/13254)
        process.on('SIGPIPE', () => {
            console.error(new Error('Unexpected SIGPIPE'));
        });
        /**
         * Kill the current process tree on exit.
         */
        function signalHandler(signal) {
            process.exit(1);
        }
        // Handles normal process termination.
        process.on('exit', () => this.onStop());
        // Handles `Ctrl+C`.
        process.on('SIGINT', signalHandler);
        // Handles `kill pid`.
        process.on('SIGTERM', signalHandler);
    }
    async initialize() {
        for (const contribution of this.contributionsProvider.getContributions()) {
            if (contribution.initialize) {
                try {
                    await this.measure(contribution.constructor.name + '.initialize', () => contribution.initialize());
                }
                catch (error) {
                    console.error('Could not initialize contribution', error);
                }
            }
        }
    }
    init() {
        this.configure();
    }
    async configure() {
        // Do not await the initialization because contributions are expected to handle
        // concurrent initialize/configure in undefined order if they provide both
        this.initialize();
        this.app.get('*.js', this.serveGzipped.bind(this, 'text/javascript'));
        this.app.get('*.js.map', this.serveGzipped.bind(this, 'application/json'));
        this.app.get('*.css', this.serveGzipped.bind(this, 'text/css'));
        this.app.get('*.wasm', this.serveGzipped.bind(this, 'application/wasm'));
        this.app.get('*.gif', this.serveGzipped.bind(this, 'image/gif'));
        this.app.get('*.png', this.serveGzipped.bind(this, 'image/png'));
        this.app.get('*.svg', this.serveGzipped.bind(this, 'image/svg+xml'));
        for (const contribution of this.contributionsProvider.getContributions()) {
            if (contribution.configure) {
                try {
                    await this.measure(contribution.constructor.name + '.configure', () => contribution.configure(this.app));
                }
                catch (error) {
                    console.error('Could not configure contribution', error);
                }
            }
        }
    }
    use(...handlers) {
        this.app.use(...handlers);
    }
    async start(aPort, aHostname) {
        const hostname = aHostname !== undefined ? aHostname : this.cliParams.hostname;
        const port = aPort !== undefined ? aPort : this.cliParams.port;
        const deferred = new promise_util_1.Deferred();
        let server;
        if (this.cliParams.ssl) {
            if (this.cliParams.cert === undefined) {
                throw new Error('Missing --cert option, see --help for usage');
            }
            if (this.cliParams.certkey === undefined) {
                throw new Error('Missing --certkey option, see --help for usage');
            }
            let key;
            let cert;
            try {
                key = await fs.readFile(this.cliParams.certkey);
            }
            catch (err) {
                console.error("Can't read certificate key");
                throw err;
            }
            try {
                cert = await fs.readFile(this.cliParams.cert);
            }
            catch (err) {
                console.error("Can't read certificate");
                throw err;
            }
            server = https.createServer({ key, cert }, this.app);
        }
        else {
            server = http.createServer(this.app);
        }
        server.on('error', error => {
            deferred.reject(error);
            /* The backend might run in a separate process,
             * so we defer `process.exit` to let time for logging in the parent process */
            setTimeout(process.exit, 0, 1);
        });
        server.listen(port, hostname, () => {
            const scheme = this.cliParams.ssl ? 'https' : 'http';
            console.info(`Theia app listening on ${scheme}://${hostname || 'localhost'}:${server.address().port}.`);
            deferred.resolve(server);
        });
        /* Allow any number of websocket servers.  */
        server.setMaxListeners(0);
        for (const contribution of this.contributionsProvider.getContributions()) {
            if (contribution.onStart) {
                try {
                    await this.measure(contribution.constructor.name + '.onStart', () => contribution.onStart(server));
                }
                catch (error) {
                    console.error('Could not start contribution', error);
                }
            }
        }
        return this.stopwatch.startAsync('server', 'Finished starting backend application', () => deferred.promise);
    }
    onStop() {
        console.info('>>> Stopping backend contributions...');
        for (const contrib of this.contributionsProvider.getContributions()) {
            if (contrib.onStop) {
                try {
                    contrib.onStop(this.app);
                }
                catch (error) {
                    console.error('Could not stop contribution', error);
                }
            }
        }
        console.info('<<< All backend contributions have been stopped.');
        this.processUtils.terminateProcessTree(process.pid);
    }
    async serveGzipped(contentType, req, res, next) {
        const acceptedEncodings = req.acceptsEncodings();
        const gzUrl = `${req.url}.gz`;
        const gzPath = path.join(this.applicationPackage.projectPath, 'lib', gzUrl);
        if (acceptedEncodings.indexOf('gzip') === -1 || !(await fs.pathExists(gzPath))) {
            next();
            return;
        }
        req.url = gzUrl;
        res.set('Content-Encoding', 'gzip');
        res.set('Content-Type', contentType);
        next();
    }
    async measure(name, fn) {
        return this.stopwatch.startAsync(name, `Backend ${name}`, fn, { thresholdMillis: TIMER_WARNING_THRESHOLD });
    }
    handleUncaughtError(error) {
        if (error) {
            console.error('Uncaught Exception: ', error.toString());
            if (error.stack) {
                console.error(error.stack);
            }
        }
    }
};
__decorate([
    (0, inversify_1.inject)(application_package_1.ApplicationPackage),
    __metadata("design:type", application_package_1.ApplicationPackage)
], BackendApplication.prototype, "applicationPackage", void 0);
__decorate([
    (0, inversify_1.inject)(process_utils_1.ProcessUtils),
    __metadata("design:type", process_utils_1.ProcessUtils)
], BackendApplication.prototype, "processUtils", void 0);
__decorate([
    (0, inversify_1.inject)(common_1.Stopwatch),
    __metadata("design:type", common_1.Stopwatch)
], BackendApplication.prototype, "stopwatch", void 0);
__decorate([
    (0, inversify_1.postConstruct)(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], BackendApplication.prototype, "init", null);
BackendApplication = __decorate([
    (0, inversify_1.injectable)(),
    __param(0, (0, inversify_1.inject)(common_1.ContributionProvider)),
    __param(0, (0, inversify_1.named)(exports.BackendApplicationContribution)),
    __param(1, (0, inversify_1.inject)(BackendApplicationCliContribution)),
    __metadata("design:paramtypes", [Object, BackendApplicationCliContribution])
], BackendApplication);
exports.BackendApplication = BackendApplication;
//# sourceMappingURL=backend-application.js.map