/********************************************************************************
 * Copyright (C) 2018 Red Hat, Inc. 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 { inject , injectable , named } from '@theia/core/shared/inversify' ; import * as cp from 'child_process' ; import * as fs from '@theia/core/shared/fs-extra' ; import * as net from 'net' ; import * as path from 'path' ; import * as request from 'request' ; import URI from '@theia/core/lib/common/uri' ; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider' ; import { HostedPluginUriPostProcessor , HostedPluginUriPostProcessorSymbolName } from './hosted-plugin-uri-postprocessor' ; import { environment , isWindows } from '@theia/core' ; import { FileUri } from '@theia/core/lib/node/file-uri' ; import { LogType } from '@theia/plugin-ext/lib/common/types' ; import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin' ; import { MetadataScanner } from '@theia/plugin-ext/lib/hosted/node/metadata-scanner' ; import { DebugPluginConfiguration } from '@theia/debug/lib/browser/debug-contribution' ; import { HostedPluginProcess } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-process' ; const DEFAULT_HOSTED_PLUGIN_PORT = 3030 ; export const HostedInstanceManager = Symbol ( 'HostedInstanceManager' ) ; /**
 * Is responsible for running and handling separate Theia instance with given plugin.
 */ export interface HostedInstanceManager { /**
     * Checks whether hosted instance is run.
     */ isRunning ( ) : boolean ; param pluginUri param port returns run ( pluginUri , port ? ) : Promise < URI > ; param pluginUri param debugConfig returns debug ( pluginUri , debugConfig ) : Promise < URI > ; /**
     * Terminates hosted plugin instance.
     * Throws error if instance is not running.
     */ terminate ( ) : void ; /**
     * Returns uri where hosted instance is run.
     * Throws error if instance is not running.
     */ getInstanceURI ( ) : URI ; /**
     * Returns uri where plugin loaded into hosted instance is located.
     * Throws error if instance is not running.
     */ getPluginURI ( ) : URI ; param uri isPluginValid ( uri ) : boolean ; } const HOSTED_INSTANCE_START_TIMEOUT_MS = 30000 ; const THEIA_INSTANCE_REGEX = /.*Theia app listening on (.*).*\./ ; const PROCESS_OPTIONS = { cwd : process . cwd ( ) , env : { ... process . env } } ; @ injectable ( ) export abstract class AbstractHostedInstanceManager implements HostedInstanceManager { protected hostedInstanceProcess : cp . ChildProcess ; protected isPluginRunning : boolean = false ; protected instanceUri : URI ; protected pluginUri : URI ; protected instanceOptions : object ; @ inject ( HostedPluginSupport ) protected readonly hostedPluginSupport : HostedPluginSupport ; @ inject ( MetadataScanner ) protected readonly metadata : MetadataScanner ; @ inject ( HostedPluginProcess ) protected readonly hostedPluginProcess : HostedPluginProcess ; isRunning ( ) { return this . isPluginRunning ; } async run ( pluginUri , port ? ) { return this . doRun ( pluginUri , port ) ; } async debug ( pluginUri , debugConfig ) { return this . doRun ( pluginUri , undefined , debugConfig ) ; } private async doRun ( pluginUri , port ? , debugConfig ? ) { if ( this . isPluginRunning ) { this . hostedPluginSupport . sendLog ( { data : 'Hosted plugin instance is already running.' , type : LogType . Info } ) ; throw new Error ( 'Hosted instance is already running.' ) ; } let command ; let processOptions ; if ( pluginUri . scheme === 'file' ) { processOptions = { ... PROCESS_OPTIONS } ; processOptions . env ! . HOSTED_PLUGIN = FileUri . fsPath ( pluginUri . toString ( ) ) ; processOptions . env ! . THEIA_PLUGINS = '' ; command = await this . getStartCommand ( port , debugConfig ) ; } else { throw new Error ( 'Not supported plugin location: ' + pluginUri . toString ( ) ) ; } this . instanceUri = await this . postProcessInstanceUri ( await this . runHostedPluginTheiaInstance ( command , processOptions ) ) ; this . pluginUri = pluginUri ; this . instanceOptions = { followRedirect : false } ; this . instanceOptions = await this . postProcessInstanceOptions ( this . instanceOptions ) ; await this . checkInstanceUriReady ( ) ; return this . instanceUri ; } terminate ( ) { if ( this . isPluginRunning ) { this . hostedPluginProcess . killProcessTree ( this . hostedInstanceProcess . pid ) ; this . hostedPluginSupport . sendLog ( { data : 'Hosted instance has been terminated' , type : LogType . Info } ) ; this . isPluginRunning = false ; } else { throw new Error ( 'Hosted plugin instance is not running.' ) ; } } getInstanceURI ( ) { if ( this . isPluginRunning ) { return this . instanceUri ; } throw new Error ( 'Hosted plugin instance is not running.' ) ; } getPluginURI ( ) { if ( this . isPluginRunning ) { return this . pluginUri ; } throw new Error ( 'Hosted plugin instance is not running.' ) ; } /**
     * Checks that the `instanceUri` is responding before exiting method
     */ public async checkInstanceUriReady ( ) { return new Promise < void > ( ( resolve , reject ) => this . pingLoop ( 60 , resolve , reject ) ) ; } param remainingCount param resolve param reject private async pingLoop ( remainingCount , resolve , reject ) { const isOK = await this . ping ( ) ; if ( isOK ) { resolve ( ) ; } else { if ( remainingCount > 0 ) { setTimeout ( ( ) => this . pingLoop ( -- remainingCount , resolve , reject ) , 1000 ) ; } else { reject ( new Error ( 'Unable to ping the remote server' ) ) ; } } } /**
     * Ping the plugin URI (checking status of the head)
     */ private async ping ( ) { return new Promise < boolean > ( ( resolve , reject ) => { const url = this . instanceUri . toString ( ) ; request . head ( url , this . instanceOptions ) . on ( 'response' , res => { resolve ( res . statusCode === 200 ) ; } ) . on ( 'error' , error => { resolve ( false ) ; } ) ; } ) ; } isPluginValid ( uri ) { const pckPath = path . join ( FileUri . fsPath ( uri ) , 'package.json' ) ; if ( fs . existsSync ( pckPath ) ) { const pck = fs . readJSONSync ( pckPath ) ; try { this . metadata . getScanner ( pck ) ; return true ; } catch ( e ) { console . error ( e ) ; return false ; } } return false ; } protected async getStartCommand ( port ? , debugConfig ? ) { const processArguments = process . argv ; let command ; if ( environment . electron . is ( ) ) { command = [ 'yarn' , 'theia' , 'start' ] ; } else { command = processArguments . filter ( ( arg , index , args ) => { if ( arg . startsWith ( '--port' ) || args [ index - 1 ] === '--port' ) { return ; } else { return arg ; } } ) ; } if ( process . env . HOSTED_PLUGIN_HOSTNAME ) { command . push ( '--hostname=' + process . env . HOSTED_PLUGIN_HOSTNAME ) ; } if ( port ) { await this . validatePort ( port ) ; command . push ( '--port=' + port ) ; } if ( debugConfig ) { command . push ( `--hosted-plugin-${ debugConfig . debugMode || 'inspect' }=0.0.0.0${ debugConfig . debugPort ? ':' + debugConfig . debugPort : '' }` ) ; } return command ; } protected async postProcessInstanceUri ( uri ) { return uri ; } protected async postProcessInstanceOptions ( options ) { return options ; } protected runHostedPluginTheiaInstance ( command , options ) { this . isPluginRunning = true ; return new Promise ( ( resolve , reject ) => { let started = false ; const outputListener = ( data ) => { const line = data . toString ( ) ; const match = THEIA_INSTANCE_REGEX . exec ( line ) ; if ( match ) { this . hostedInstanceProcess . stdout ! . removeListener ( 'data' , outputListener ) ; started = true ; resolve ( new URI ( match [ 1 ] ) ) ; } } ; if ( isWindows ) { options . shell = true ; } this . hostedInstanceProcess = cp . spawn ( command . shift ( ) ! , command , options ) ; this . hostedInstanceProcess . on ( 'error' , ( ) => { this . isPluginRunning = false ; } ) ; this . hostedInstanceProcess . on ( 'exit' , ( ) => { this . isPluginRunning = false ; } ) ; this . hostedInstanceProcess . stdout ! . addListener ( 'data' , outputListener ) ; this . hostedInstanceProcess . stdout ! . addListener ( 'data' , data => { this . hostedPluginSupport . sendLog ( { data : data . toString ( ) , type : LogType . Info } ) ; } ) ; this . hostedInstanceProcess . stderr ! . addListener ( 'data' , data => { this . hostedPluginSupport . sendLog ( { data : data . toString ( ) , type : LogType . Error } ) ; } ) ; setTimeout ( ( ) => { if ( ! started ) { this . terminate ( ) ; this . isPluginRunning = false ; reject ( new Error ( 'Timeout.' ) ) ; } } , HOSTED_INSTANCE_START_TIMEOUT_MS ) ; } ) ; } protected async validatePort ( port ) { if ( port < 1 || port > 65535 ) { throw new Error ( 'Port value is incorrect.' ) ; } if ( ! await this . isPortFree ( port ) ) { throw new Error ( 'Port ' + port + ' is already in use.' ) ; } } protected isPortFree ( port ) { return new Promise ( resolve => { const server = net . createServer ( ) ; server . listen ( port , '0.0.0.0' ) ; server . on ( 'error' , ( ) => { resolve ( false ) ; } ) ; server . on ( 'listening' , ( ) => { server . close ( ) ; resolve ( true ) ; } ) ; } ) ; } } @ injectable ( ) export class NodeHostedPluginRunner extends AbstractHostedInstanceManager { @ inject ( ContributionProvider ) @ named ( Symbol . for ( HostedPluginUriPostProcessorSymbolName ) ) protected readonly uriPostProcessors : ContributionProvider < HostedPluginUriPostProcessor > ; protected async postProcessInstanceUri ( uri ) { for ( const uriPostProcessor of this . uriPostProcessors . getContributions ( ) ) { uri = await uriPostProcessor . processUri ( uri ) ; } return uri ; } protected async postProcessInstanceOptions ( options ) { for ( const uriPostProcessor of this . uriPostProcessors . getContributions ( ) ) { options = await uriPostProcessor . processOptions ( options ) ; } return options ; } protected async getStartCommand ( port ? , debugConfig ? ) { if ( ! port ) { port = process . env . HOSTED_PLUGIN_PORT ? Number ( process . env . HOSTED_PLUGIN_PORT ) : ( debugConfig ?. debugPort ? Number ( debugConfig . debugPort ) : DEFAULT_HOSTED_PLUGIN_PORT ) ; } return super . getStartCommand ( port , debugConfig ) ; } } @ injectable ( ) export class ElectronNodeHostedPluginRunner extends AbstractHostedInstanceManager { }