import * as localtunnel from 'localtunnel' ; import { BinaryDataManager , IBinaryDataConfig , TUNNEL_SUBDOMAIN_ENV , UserSettings } from 'n8n-core' ; import { Command , flags } from '@oclif/command' ; import * as Redis from 'ioredis' ; import { IDataObject , LoggerProxy } from 'n8n-workflow' ; import * as config from '../config' ; import { ActiveExecutions , ActiveWorkflowRunner , CredentialsOverwrites , CredentialTypes , DatabaseType , Db , ExternalHooks , GenericHelpers , InternalHooksManager , LoadNodesAndCredentials , NodeTypes , Server , TestWebhooks , WaitTracker , } from '../src' ; import { getLogger } from '../src/Logger' ; const open = require ( 'open' ) ; let activeWorkflowRunner ; let processExitCode = 0 ; export class Start extends Command { static description = 'Starts n8n. Makes Web-UI available and starts active workflows' ; static examples = [ `$ n8n start` , `$ n8n start --tunnel` , `$ n8n start -o` , `$ n8n start --tunnel -o` , ] ; static flags = { help : flags . help ( { char : 'h' } ) , open : flags . boolean ( { char : 'o' , description : 'opens the UI automatically in browser' , } ) , tunnel : flags . boolean ( { description : 'runs the webhooks via a hooks.n8n.cloud tunnel server. Use only for testing and development!' , } ) , } ; /**
     * Opens the UI in browser
     */ static openBrowser ( ) { const editorUrl = GenericHelpers . getBaseUrl ( ) ; open ( editorUrl , { wait : true } ) . catch ( ( error ) => { console . log ( `\nWas not able to open URL in browser. Please open manually by visiting:\n${ editorUrl }\n` ) ; } ) ; } /**
     * Stoppes the n8n in a graceful way.
     * Make for example sure that all the webhooks from third party services
     * get removed.
     */ static async stopProcess ( ) { getLogger ( ) . info ( '\nStopping n8n...' ) ; try { const externalHooks = ExternalHooks ( ) ; await externalHooks . run ( 'n8n.stop' , [ ] ) ; setTimeout ( ( ) => { console . log ( `process exited after 30s` ) ; process . exit ( processExitCode ) ; } , 30000 ) ; await InternalHooksManager . getInstance ( ) . onN8nStop ( ) ; const skipWebhookDeregistration = config . get ( 'endpoints.skipWebhoooksDeregistrationOnShutdown' ) as boolean ; const removePromises = [ ] ; if ( activeWorkflowRunner !== undefined && ! skipWebhookDeregistration ) { removePromises . push ( activeWorkflowRunner . removeAll ( ) ) ; } const testWebhooks = TestWebhooks . getInstance ( ) ; removePromises . push ( testWebhooks . removeAll ( ) ) ; await Promise . all ( removePromises ) ; const activeExecutionsInstance = ActiveExecutions . getInstance ( ) ; let executingWorkflows = activeExecutionsInstance . getActiveExecutions ( ) ; let count = 0 ; while ( executingWorkflows . length !== 0 ) { if ( count ++ % 4 === 0 ) { console . log ( `Waiting for ${ executingWorkflows . length } active executions to finish...` ) ; executingWorkflows . map ( ( execution ) => { console . log ( ` - Execution ID ${ execution . id }, workflow ID: ${ execution . workflowId }` ) ; } ) ; } await new Promise ( ( resolve ) => { setTimeout ( resolve , 500 ) ; } ) ; executingWorkflows = activeExecutionsInstance . getActiveExecutions ( ) ; } } catch ( error ) { console . error ( 'There was an error shutting down n8n.' , error ) ; } process . exit ( processExitCode ) ; } async run ( ) { process . on ( 'SIGTERM' , Start . stopProcess ) ; process . on ( 'SIGINT' , Start . stopProcess ) ; const { flags } = this . parse ( Start ) ; await ( async ( ) => { try { const logger = getLogger ( ) ; LoggerProxy . init ( logger ) ; logger . info ( 'Initializing n8n process' ) ; const startDbInitPromise = Db . init ( ) . catch ( ( error ) => { logger . error ( `There was an error initializing DB: "${ error . message }"` ) ; processExitCode = 1 ; process . emit ( 'SIGINT' ) ; process . exit ( 1 ) ; } ) ; const userSettings = await UserSettings . prepareUserSettings ( ) ; const loadNodesAndCredentials = LoadNodesAndCredentials ( ) ; await loadNodesAndCredentials . init ( ) ; const externalHooks = ExternalHooks ( ) ; await externalHooks . init ( ) ; const nodeTypes = NodeTypes ( ) ; await nodeTypes . init ( loadNodesAndCredentials . nodeTypes ) ; const credentialTypes = CredentialTypes ( ) ; await credentialTypes . init ( loadNodesAndCredentials . credentialTypes ) ; const credentialsOverwrites = CredentialsOverwrites ( ) ; await credentialsOverwrites . init ( ) ; await startDbInitPromise ; if ( config . get ( 'executions.mode' ) === 'queue' ) { const redisHost = config . get ( 'queue.bull.redis.host' ) ; const redisPassword = config . get ( 'queue.bull.redis.password' ) ; const redisPort = config . get ( 'queue.bull.redis.port' ) ; const redisDB = config . get ( 'queue.bull.redis.db' ) ; const redisConnectionTimeoutLimit = config . get ( 'queue.bull.redis.timeoutThreshold' ) ; let lastTimer = 0 ; let cumulativeTimeout = 0 ; const settings = { retryStrategy : ( times ) : number | null => { const now = Date . now ( ) ; if ( now - lastTimer > 30000 ) { lastTimer = now ; cumulativeTimeout = 0 ; } else { cumulativeTimeout += now - lastTimer ; lastTimer = now ; if ( cumulativeTimeout > redisConnectionTimeoutLimit ) { logger . error ( `Unable to connect to Redis after ${ redisConnectionTimeoutLimit }. Exiting process.` ) ; process . exit ( 1 ) ; } } return 500 ; } , } as IDataObject ; if ( redisHost ) { settings . host = redisHost ; } if ( redisPassword ) { settings . password = redisPassword ; } if ( redisPort ) { settings . port = redisPort ; } if ( redisDB ) { settings . db = redisDB ; } const redis = new Redis ( settings ) ; redis . on ( 'error' , ( error ) => { if ( error . toString ( ) . includes ( 'ECONNREFUSED' ) === true ) { logger . warn ( 'Redis unavailable - trying to reconnect...' ) ; } else { logger . warn ( 'Error with Redis: ' , error ) ; } } ) ; } const dbType = ( await GenericHelpers . getConfigValue ( 'database.type' ) ) as DatabaseType ; if ( dbType === 'sqlite' ) { const shouldRunVacuum = config . get ( 'database.sqlite.executeVacuumOnStartup' ) as number ; if ( shouldRunVacuum ) { await Db . collections . Execution ! . query ( 'VACUUM;' ) ; } } if ( flags . tunnel ) { this . log ( '\nWaiting for tunnel ...' ) ; let tunnelSubdomain ; if ( process . env [ TUNNEL_SUBDOMAIN_ENV ] !== undefined && process . env [ TUNNEL_SUBDOMAIN_ENV ] !== '' ) { tunnelSubdomain = process . env [ TUNNEL_SUBDOMAIN_ENV ] ; } else if ( userSettings . tunnelSubdomain !== undefined ) { tunnelSubdomain = userSettings . tunnelSubdomain ; } if ( tunnelSubdomain === undefined ) { const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789' ; userSettings . tunnelSubdomain = Array . from ( { length : 24 } ) . map ( ( ) => { return availableCharacters . charAt ( Math . floor ( Math . random ( ) * availableCharacters . length ) ) ; } ) . join ( '' ) ; await UserSettings . writeUserSettings ( userSettings ) ; } const tunnelSettings = { host : 'https://hooks.n8n.cloud' , subdomain : tunnelSubdomain , } ; const port = config . get ( 'port' ) ; const webhookTunnel = await localtunnel ( port , tunnelSettings ) ; process . env . WEBHOOK_URL = `${ webhookTunnel . url }/` ; this . log ( `Tunnel URL: ${ process . env . WEBHOOK_URL }\n` ) ; this . log ( 'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!' ) ; } const instanceId = await UserSettings . getInstanceId ( ) ; const { cli } = await GenericHelpers . getVersions ( ) ; InternalHooksManager . init ( instanceId , cli , nodeTypes ) ; const binaryDataConfig = config . get ( 'binaryDataManager' ) as IBinaryDataConfig ; await BinaryDataManager . init ( binaryDataConfig , true ) ; await Server . start ( ) ; activeWorkflowRunner = ActiveWorkflowRunner . getInstance ( ) ; await activeWorkflowRunner . init ( ) ; WaitTracker ( ) ; const editorUrl = GenericHelpers . getBaseUrl ( ) ; this . log ( `\nEditor is now accessible via:\n${ editorUrl }` ) ; if ( Boolean ( process . stdout . isTTY ) && process . stdin . setRawMode ) { process . stdin . setRawMode ( true ) ; process . stdin . resume ( ) ; process . stdin . setEncoding ( 'utf8' ) ; let inputText = '' ; if ( flags . open ) { Start . openBrowser ( ) ; } this . log ( `\nPress "o" to open in Browser.` ) ; process . stdin . on ( 'data' , ( key ) => { if ( key === 'o' ) { Start . openBrowser ( ) ; inputText = '' ; } else if ( key . charCodeAt ( 0 ) === 3 ) { Start . stopProcess ( ) ; } else { if ( key . charCodeAt ( 0 ) === 13 ) { process . stdout . write ( '\n' ) ; inputText = '' ; } else { inputText += key ; process . stdout . write ( key ) ; } } } ) ; } } catch ( error ) { this . error ( `There was an error: ${ error . message }` ) ; processExitCode = 1 ; process . emit ( 'SIGINT' ) ; } } ) ( ) ; } }