/********************************************************************************
 * 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
 ********************************************************************************/ import * as path from 'path' ; import * as http from 'http' ; import * as https from 'https' ; import * as express from 'express' ; import * as yargs from 'yargs' ; import * as fs from 'fs-extra' ; import { performance , PerformanceObserver } from 'perf_hooks' ; import { inject , named , injectable , postConstruct } from 'inversify' ; import { ContributionProvider , MaybePromise } from '../common' ; import { CliContribution } from './cli' ; import { Deferred } from '../common/promise-util' ; import { environment } from '../common/index' ; import { AddressInfo } from 'net' ; import { ApplicationPackage } from '@theia/application-package' ; import { ProcessUtils } from './process-utils' ; const APP_PROJECT_PATH = 'app-project-path' ; const TIMER_WARNING_THRESHOLD = 50 ; const DEFAULT_PORT = environment . electron . is ( ) ? 0 : 3000 ; const DEFAULT_HOST = 'localhost' ; const DEFAULT_SSL = false ; export const BackendApplicationServer = Symbol ( 'BackendApplicationServer' ) ; /**
 * This service is responsible for serving the frontend files.
 *
 * When not bound, `@theia/cli` generators will bind it on the fly to serve files according to its own layout.
 */ export interface BackendApplicationServer extends BackendApplicationContribution { } export const BackendApplicationContribution = Symbol ( 'BackendApplicationContribution' ) ; /**
 * Contribution for hooking into the backend lifecycle.
 */ export interface BackendApplicationContribution { returns initialize ? ( ) : MaybePromise < void > ; param app returns configure ? ( app ) : MaybePromise < void > ; param server returns onStart ? ( server ) : MaybePromise < void > ; param app onStop ? ( app ? ) : void ; } @ injectable ( ) export class BackendApplicationCliContribution implements CliContribution { port : number ; hostname : string | undefined ; ssl : boolean | undefined ; cert : string | undefined ; certkey : string | undefined ; projectPath : string ; 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 as number ; this . hostname = args . hostname as string ; this . ssl = args . ssl as boolean ; this . cert = args . cert as string ; this . certkey = args . certkey as string ; this . projectPath = args [ APP_PROJECT_PATH ] as string ; } protected appProjectPath ( ) { if ( 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 ( ) ; } } /**
 * The main entry point for Theia applications.
 */ @ injectable ( ) export class BackendApplication { protected readonly app : express . Application = express ( ) ; @ inject ( ApplicationPackage ) protected readonly applicationPackage : ApplicationPackage ; @ inject ( ProcessUtils ) protected readonly processUtils : ProcessUtils ; private readonly _performanceObserver : PerformanceObserver ; constructor ( @ inject ( ContributionProvider ) @ named ( BackendApplicationContribution ) protected readonly contributionsProvider , @ inject ( BackendApplicationCliContribution ) protected readonly cliParams ) { process . on ( 'uncaughtException' , error => { if ( error ) { console . error ( 'Uncaught Exception: ' , error . toString ( ) ) ; if ( error . stack ) { console . error ( error . stack ) ; } } } ) ; process . on ( 'SIGPIPE' , ( ) => { console . error ( new Error ( 'Unexpected SIGPIPE' ) ) ; } ) ; /**
         * Kill the current process tree on exit.
         */ function signalHandler ( signal ) { process . exit ( 1 ) ; } process . on ( 'exit' , ( ) => this . onStop ( ) ) ; process . on ( 'SIGINT' , signalHandler ) ; process . on ( 'SIGTERM' , signalHandler ) ; this . _performanceObserver = new PerformanceObserver ( list => { for ( const item of list . getEntries ( ) ) { const contribution = `Backend ${ item . name }` ; if ( item . duration > TIMER_WARNING_THRESHOLD ) { console . warn ( `${ contribution } is slow, took: ${ item . duration . toFixed ( 1 ) } ms` ) ; } else { console . debug ( `${ contribution } took: ${ item . duration . toFixed ( 1 ) } ms` ) ; } } } ) ; this . _performanceObserver . observe ( { entryTypes : [ 'measure' ] } ) ; this . initialize ( ) ; } protected 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 ) ; } } } } @ postConstruct ( ) protected async configure ( ) { 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 Deferred < http . Server | https . Server > ( ) ; 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 as string ) ; } catch ( err ) { console . error ( "Can't read certificate key" ) ; throw err ; } try { cert = await fs . readFile ( this . cliParams . cert as string ) ; } 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 ) ; 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 ( ) as AddressInfo ) . port }.` ) ; deferred . resolve ( server ) ; } ) ; 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 deferred . promise ; } protected 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 . _performanceObserver . disconnect ( ) ; this . processUtils . terminateProcessTree ( process . pid ) ; } protected 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 ( ) ; } protected async measure < T > ( name , fn ) { const startMark = name + '-start' ; const endMark = name + '-end' ; performance . mark ( startMark ) ; const result = await fn ( ) ; performance . mark ( endMark ) ; performance . measure ( name , startMark , endMark ) ; performance . clearMarks ( name ) ; return result ; } }