import axios from 'axios' ; import { ChildProcess , execSync , spawn } from 'child_process' ; import DecompressZip from 'decompress-zip' ; import * as fs from 'fs' ; import * as path from 'path' ; import type { Stream } from 'stream' ; import tar from 'tar' ; import { INotificationService } from './INotificationService' ; import { ErrorAction } from './models/ErrorAction' ; import { FileDefinition } from './models/FileDefinition' ; import { PluginDefinition } from './models/PluginDefinition' ; import { PluginStatus } from './models/PluginStatus' ; export class Plugin { private _status : PluginStatus = PluginStatus . Unknown ; private process ? : ChildProcess ; private stopCalled : boolean = false ; /** The path to the location of the installed plugin */ public readonly pluginDirectory : string ; get name ( ) : string { return this . pluginDefinition . name ; } get status ( ) : PluginStatus { return this . _status ; } set status ( status ) { this . _status = status ; this . notificationService . sendStatus ( { name : this . name , status : this . _status , } ) ; } constructor ( private readonly baseDirectory , private readonly pluginDefinition , private readonly notificationService ) { this . pluginDirectory = path . join ( baseDirectory , this . name ) ; } /** Starts the plugin */ public start = async ( ) => { let installed = this . checkInstall ( ) ; if ( ! installed ) { try { await this . install ( ) ; } catch ( err ) { console . error ( `Unable to install ${ this . name }` ) ; console . error ( JSON . stringify ( err ) ) ; } } this . status = PluginStatus . Initializing ; this . stopCalled = false ; if ( this . pluginDefinition . extraFiles ) { await this . addExtraFiles ( this . pluginDefinition . extraFiles ) ; } let exePath = path . join ( this . pluginDirectory , this . pluginDefinition . exe ) ; console . log ( `Starting ${ exePath }` ) ; let ls = spawn ( `"${ exePath }"` , [ this . pluginDefinition . args || '' ] , { shell : true , windowsHide : true , } ) ; this . process = ls ; if ( ls . stdout ) { ls . stdout . on ( 'data' , ( data ) => { this . onOutput ( data . toString ( ) ) ; } ) ; } if ( ls . stderr ) { ls . stderr . on ( 'data' , ( data ) => { this . onOutput ( data . toString ( ) ) ; } ) ; } ls . on ( 'exit' , this . onExit ) ; } ; /** Stops the plugin */ public stop = async ( ) => { this . status = PluginStatus . Stopped ; this . stopCalled = true ; if ( this . process && ! this . process . killed ) { this . process . kill ( ) ; if ( process . platform === 'win32' ) { const processName = path . basename ( this . pluginDefinition . exe ) ; setTimeout ( ( ) => { try { execSync ( `taskkill /im ${ processName } /t /f` ) ; } catch { } } , 200 ) ; } else if ( process . platform === 'linux' ) { const processName = path . basename ( this . pluginDefinition . exe ) ; setTimeout ( ( ) => { try { execSync ( `pkill ${ processName }` ) ; } catch { } } , 200 ) ; } } else { console . log ( 'No process to kill.' ) ; } this . process = undefined ; } ; /** Restarts the plugin */ restart = async ( ) => { await this . stop ( ) ; await this . start ( ) ; } ; /** Checks to see if a plugin has been installed correctly */ private checkInstall = ( ) : boolean => { console . log ( `Checking ${ this . name } installation at ${ this . pluginDirectory }` ) ; let installCorrect = this . fileExists ( this . pluginDefinition . exe ) ; if ( installCorrect && this . pluginDefinition . fileChecks ) installCorrect = this . pluginDefinition . fileChecks . every ( this . fileExists ) ; console . log ( `Plugin installed ${ installCorrect ? 'correctly' : 'incorrectly' }` ) ; return installCorrect ; } ; /** Check to see if the given file exists*/ private fileExists = ( file ) : boolean => { let fullPath = path . join ( this . pluginDirectory , file ) ; let exists = fs . existsSync ( fullPath ) ; console . log ( `Checked exists:${ exists }) - ${ fullPath }` ) ; return exists ; } ; /** Installs the plugin */ private install = async ( ) => { console . log ( `Install started for ${ this . name }` ) ; this . status = PluginStatus . Installing ; let filename = path . basename ( this . pluginDefinition . downloadUrl ) ; let downloadFolder = path . join ( this . baseDirectory , '_downloads' ) ; let downloadFilename = path . join ( downloadFolder , filename ) ; console . log ( 'Installation details' , filename , downloadFolder , downloadFilename ) ; if ( ! fs . existsSync ( downloadFolder ) ) { try { console . log ( `Download directory started for ${ this . name }` ) ; await fs . promises . mkdir ( downloadFolder , { recursive : true } ) ; console . log ( `Download directory finished for ${ this . name }` ) ; } catch ( err ) { console . log ( 'Download directory creation error' , err ) ; return ; } } if ( fs . existsSync ( downloadFilename ) ) { console . log ( `Previous download removal started for ${ this . name }` ) ; await fs . promises . unlink ( downloadFilename ) ; console . log ( `Previous download removal completed for ${ this . name }` ) ; } console . log ( `Starting download for ${ this . name }` ) ; let res = await axios . get < Stream > ( this . pluginDefinition . downloadUrl , { responseType : 'stream' } ) ; await new Promise < void > ( ( fulfill ) => { let stream = res . data . pipe ( fs . createWriteStream ( downloadFilename ) ) ; stream . on ( 'finish' , fulfill ) ; } ) ; console . log ( `Download complete ${ this . name }` ) ; console . log ( `Extracting ${ this . name }` ) ; await new Promise < void > ( ( fulfill , reject ) => { if ( downloadFilename . endsWith ( '.tar.gz' ) ) { fs . promises . mkdir ( this . pluginDirectory ) . catch ( ( err ) => { if ( err . code !== 'EEXIST' ) { throw err ; } } ) . then ( ( ) => tar . extract ( { cwd : this . pluginDirectory , file : downloadFilename , } ) ) . then ( ( ) => { console . log ( 'Finished extracting' ) ; fulfill ( ) ; } ) . catch ( ( err ) => { console . log ( `Extracting ${ this . name } failed` , err ) ; reject ( err ) ; } ) ; } else { let unzipper = new DecompressZip ( downloadFilename ) ; unzipper . on ( 'error' , ( err ) => { console . log ( `Extracting ${ this . name } failed` , err ) ; reject ( err ) ; } ) ; unzipper . on ( 'extract' , ( ) => { console . log ( 'Finished extracting' ) ; fulfill ( ) ; } ) ; unzipper . extract ( { path : this . pluginDirectory , } ) ; } } ) ; console . log ( `Finished extracting ${ this . name }` ) ; console . log ( `Previous download removal started for ${ this . name }` ) ; await fs . promises . unlink ( downloadFilename ) ; console . log ( `Previous download removal completed for ${ this . name }` ) ; await this . timeout ( 1000 ) ; console . log ( `Install complete of ${ this . name }` ) ; } ; private addExtraFiles = async ( files ) => { for ( let file of files ) { let filePath = path . join ( this . pluginDirectory , file . filename ) ; if ( fs . existsSync ( filePath ) ) { await fs . promises . unlink ( filePath ) ; } let dir = path . dirname ( filePath ) ; await fs . promises . mkdir ( dir , { recursive : true } ) ; await fs . promises . writeFile ( filePath , file . contents ) ; } } ; private timeout = ( ms ) => new Promise ( ( res ) => setTimeout ( res , ms ) ) ; private onExit = async ( code ) => { console . log ( 'exit: ' + code ) ; let stopped = this . stopCalled ; this . stopCalled = false ; if ( this . pluginDefinition . autoRestart === false ) { console . log ( `Auto restart is disabled for ${ this . name }` ) ; if ( ! stopped ) { await this . stop ( ) ; } return ; } if ( this . status === PluginStatus . Stopped || stopped ) { return ; } await this . restart ( ) ; } ; private onOutput = async ( message ) => { message = message . trim ( ) ; if ( new RegExp ( this . pluginDefinition . runningCheck ) . test ( message ) ) { this . status = PluginStatus . Running ; console . log ( 'The running check was detected' ) ; } if ( this . pluginDefinition . errors ) { let error = this . pluginDefinition . errors . find ( ( x ) => new RegExp ( x . message ) . test ( message ) ) ; if ( error ) { this . notificationService . sendError ( { message : message , errorCode : error . errorCode , errorCategory : error . errorCategory , } ) ; switch ( error . errorAction ) { case ErrorAction . Stop : await this . stop ( ) ; break ; case ErrorAction . Restart : await this . restart ( ) ; break ; case ErrorAction . Ignore : default : break ; } } } console . log ( message ) ; } ; }