import * as os from 'os' ; import * as vscode from 'vscode' ; import { ComponentConfiguration } from './configuration' ; import { Settings } from './settings' ; export enum Status { UNKNOWN = 'unknown' , DISABLED = 'disabled' , INITIAL = 'initial' , STARTING = 'starting' , READY = 'ready' , STOPPING = 'stopping' , STOPPED = 'stopped' , FAILED = 'failed' , ERROR = 'error' } export interface Runnable < T extends ComponentConfiguration > { settings : Settings < T > ; status : Status ; statusErrorMessage : string | undefined ; onDidChangeStatus : vscode . Event < Status > ; start ( ) : Promise < void > ; stop ( ) : Promise < void > ; } export abstract class RunnableComponent < T extends ComponentConfiguration > implements vscode . Disposable , Runnable < T > { protected disposables : vscode . Disposable [ ] = [ ] ; private _status : Status = Status . INITIAL ; private _statusError : Error | undefined ; _onDidChangeStatus : vscode . EventEmitter < Status > = new vscode . EventEmitter < Status > ( ) ; readonly onDidChangeStatus : vscode . Event < Status > = this . _onDidChangeStatus . event ; constructor ( public readonly name , public readonly settings ) { this . disposables . push ( this . _onDidChangeStatus ) ; settings . onDidConfigurationChange ( this . handleConfigurationChanged , this , this . disposables ) ; settings . onDidConfigurationError ( this . handleConfigurationError , this , this . disposables ) ; } protected async handleConfigurationChanged ( cfg ) { await this . restart ( ) ; } protected async handleConfigurationError ( err ) { this . setError ( err ) ; } public get status ( ) : Status { return this . _status ; } public get statusErrorMessage ( ) : string | undefined { return this . _statusError ?. message ; } protected addCommand ( name , command ) { this . disposables . push ( vscode . commands . registerCommand ( name , command , this ) ) ; } protected setDisabled ( d ) { if ( d ) { this . stop ( ) ; this . setStatus ( Status . DISABLED ) ; } else { this . _status = Status . INITIAL ; this . restart ( ) ; } } protected setStatus ( status ) { if ( this . _status === status ) { return ; } if ( this . _status === Status . DISABLED ) { return ; } this . _status = status ; this . _onDidChangeStatus . fire ( status ) ; } protected setError ( err ) { this . _statusError = err ; if ( err instanceof DisabledError ) { this . setDisabled ( true ) ; } else if ( err instanceof StatusError ) { this . setStatus ( err . status ) ; } else { this . setStatus ( Status . ERROR ) ; } } async restart ( ) { await this . stop ( ) ; await this . start ( ) ; } async start ( ) { const cfg = await this . settings . get ( ) ; if ( ! cfg . enabled ) { this . setDisabled ( true ) ; return ; } if ( this . status === Status . DISABLED && cfg . enabled ) { this . setDisabled ( false ) ; return ; } this . setStatus ( Status . STARTING ) ; try { await this . startInternal ( ) ; this . setStatus ( Status . READY ) ; } catch ( e ) { if ( e instanceof Error ) { this . setError ( e ) ; } } } async stop ( ) { if ( this . _status === Status . DISABLED ) { return ; } this . setStatus ( Status . STOPPING ) ; try { await this . stopInternal ( ) ; this . setStatus ( Status . STOPPED ) ; } catch ( e ) { if ( e instanceof Error ) { this . setError ( e ) ; } } } abstract startInternal ( ) ; abstract stopInternal ( ) ; dispose ( ) { this . disposables . forEach ( d => d . dispose ( ) ) ; this . disposables . length = 0 ; } } export interface LaunchArgs { command : string [ ] ; showSuccessfulLaunchTerminal : boolean ; showFailedLaunchTerminal : boolean ; cwd ? : string ; } /**
 * LaunchableComponent implementations spawn subprocesses in a terminal.  The
 * abstract methods are used to launch the component and possibly retry if the
 * service is not running.
 */ export abstract class LaunchableComponent < T extends ComponentConfiguration > extends RunnableComponent < T > { public terminal : vscode . Terminal | undefined ; _onDidAttachTerminal : vscode . EventEmitter < vscode . Terminal > = new vscode . EventEmitter < vscode . Terminal > ( ) ; readonly onDidAttachTerminal : vscode . Event < vscode . Terminal > = this . _onDidAttachTerminal . event ; constructor ( name , public readonly settings , commandName , protected terminalName , private launchIntervalMs = 500 , private launchIterations = 25 ) { super ( name , settings ) ; this . disposables . push ( vscode . commands . registerCommand ( commandName , this . handleCommandLaunch , this ) ) ; this . disposables . push ( vscode . window . onDidCloseTerminal ( t => { if ( t . name !== this . terminalName ) { return ; } this . disposeTerminal ( ) ; setTimeout ( ( ) => { this . restart ( ) ; } , 500 ) ; } ) ) ; this . disposables . push ( vscode . window . onDidOpenTerminal ( t => { if ( t . name !== this . terminalName ) { return ; } this . attachTerminal ( t ) ; } ) ) ; vscode . window . terminals . forEach ( t => { if ( t . name !== this . terminalName ) { return ; } this . attachTerminal ( t ) ; } ) ; } param e abstract shouldLaunch ( e ) ; /**
     * launchInternal should return a promise that resolves when the component was
     * successfully launched.  If the promise rejects, shouldLaunch is called with
     * the error to determine if retry should be attempted.
     */ abstract launchInternal ( ) ; /**
     * getLaunchArgs should return the command line arguments.
     */ abstract getLaunchArgs ( ) ; setDisabled ( b ) { super . setDisabled ( b ) ; if ( b ) { this . disposeTerminal ( ) ; vscode . window . terminals . forEach ( t => { if ( t . name !== this . terminalName ) { return ; } t . dispose ( ) ; } ) ; } } protected cleanFinishedTerminals ( ) { vscode . window . terminals . forEach ( t => { if ( t . name !== this . terminalName ) { return ; } if ( t . exitStatus === undefined ) { return ; } t . dispose ( ) ; } ) ; } protected attachTerminal ( t ) { if ( t . exitStatus !== undefined ) { t . dispose ( ) ; return ; } if ( this . status === Status . DISABLED ) { t . dispose ( ) ; return ; } if ( this . terminal ) { return ; } this . terminal = t ; this . disposables . push ( t ) ; this . _onDidAttachTerminal . fire ( t ) ; } protected async handleConfigurationChanged ( cfg ) { super . handleConfigurationChanged ( cfg ) ; if ( ! cfg . enabled ) { this . disposeTerminal ( ) ; } } disposeTerminal ( ) { this . terminal ?. dispose ( ) ; this . terminal = undefined ; } async restart ( ) { await this . start ( ) ; vscode . window . terminals . forEach ( t => { if ( t . name !== this . terminalName ) { return ; } if ( t . exitStatus === undefined ) { return ; } t . dispose ( ) ; } ) ; } async startInternal ( ) { try { await this . launchInternal ( ) ; } catch ( e ) { if ( e instanceof StatusError ) { throw e ; } if ( e instanceof Error ) { if ( await this . shouldLaunch ( e ) ) { this . handleCommandLaunch ( ) ; } else { throw e ; } } } } async stopInternal ( ) { this . disposeTerminal ( ) ; } async handleCommandLaunch ( extraArgs = [ ] ) { const launchArgs = await this . getLaunchArgs ( ) ; if ( ! launchArgs ) { return ; } if ( ! this . terminal ) { console . log ( `Launching terminal "${ this . terminalName }"...` ) ; this . terminal = vscode . window . createTerminal ( this . terminalName ) ; } if ( launchArgs . showSuccessfulLaunchTerminal ) { this . terminal . show ( ) ; } const args = launchArgs . command . concat ( extraArgs ) ; if ( args [ 0 ] . indexOf ( ' ' ) >= 0 ) { if ( os . platform ( ) === 'win32' ) { args [ 0 ] = args [ 0 ] . replace ( / /g , '` ' ) ; } else { args [ 0 ] = `'${ args [ 0 ] }'` ; } } const command = args . join ( ' ' ) ; this . terminal . sendText ( command , true ) ; this . setStatus ( Status . STARTING ) ; return new Promise ( ( resolve , reject ) => { let iteration = this . launchIterations ; const timeout = setInterval ( async ( ) => { try { await this . launchInternal ( ) ; clearTimeout ( timeout ) ; this . handleLaunchSuccess ( launchArgs , this . terminal ! ) ; resolve ( ) ; } catch ( err ) { if ( -- iteration <= 0 ) { clearTimeout ( timeout ) ; this . handleLaunchFailed ( launchArgs , this . terminal ! ) ; reject ( err ) ; return ; } } } , this . launchIntervalMs ) ; } ) ; } handleLaunchSuccess ( launchArgs , terminal ) { this . setStatus ( Status . READY ) ; this . cleanFinishedTerminals ( ) ; if ( launchArgs . showSuccessfulLaunchTerminal ) { terminal . show ( ) ; } else { terminal . hide ( ) ; } } handleLaunchFailed ( launchArgs , terminal ) { console . warn ( `"${ this . terminalName }" failed to launch.  Please check the terminal where it was started for more information.` ) ; let exitStatusDetail = '' ; if ( terminal . exitStatus ) { exitStatusDetail = ` (exit status ${ terminal . exitStatus . code })` ; } this . setError ( new Error ( `Failed to start (timeout)${ exitStatusDetail }` ) ) ; if ( launchArgs . showFailedLaunchTerminal ) { terminal . show ( ) ; } else { terminal . hide ( ) ; } } } export class DisabledError extends Error { constructor ( msg ) { super ( msg ) ; } } export class StatusError extends Error { constructor ( msg , public readonly status ) { super ( msg ) ; } }