2019-08-28 06:28:47 -07:00
import * as localtunnel from 'localtunnel' ;
import {
2019-10-14 22:21:15 -07:00
TUNNEL_SUBDOMAIN_ENV ,
2019-08-28 06:28:47 -07:00
UserSettings ,
2020-03-16 01:58:49 -07:00
} from 'n8n-core' ;
2019-08-28 06:28:47 -07:00
import { Command , flags } from '@oclif/command' ;
2019-06-23 03:35:23 -07:00
const open = require ( 'open' ) ;
2021-02-08 23:59:32 -08:00
import * as Redis from 'ioredis' ;
2019-06-23 03:35:23 -07:00
2021-06-23 02:20:07 -07:00
import * as config from '../config' ;
2019-06-23 03:35:23 -07:00
import {
2020-09-14 03:30:58 -07:00
ActiveExecutions ,
2019-06-23 03:35:23 -07:00
ActiveWorkflowRunner ,
2020-01-25 23:48:38 -08:00
CredentialsOverwrites ,
2020-10-22 06:46:03 -07:00
CredentialTypes ,
2020-12-31 01:42:16 -08:00
DatabaseType ,
2019-06-23 03:35:23 -07:00
Db ,
2020-05-03 23:56:01 -07:00
ExternalHooks ,
2019-06-23 03:35:23 -07:00
GenericHelpers ,
2021-03-25 03:23:54 -07:00
IExecutionsCurrentSummary ,
2019-06-23 03:35:23 -07:00
LoadNodesAndCredentials ,
NodeTypes ,
Server ,
2019-10-14 22:21:15 -07:00
TestWebhooks ,
2021-08-21 05:11:32 -07:00
WaitTracker ,
2021-03-25 03:23:54 -07:00
} from '../src' ;
2021-02-08 23:59:32 -08:00
import { IDataObject } from 'n8n-workflow' ;
2019-06-23 03:35:23 -07:00
2021-08-21 05:11:32 -07:00
import {
2021-05-01 20:43:01 -07:00
getLogger ,
} from '../src/Logger' ;
import {
LoggerProxy ,
} from 'n8n-workflow' ;
2019-06-23 03:35:23 -07:00
let activeWorkflowRunner : ActiveWorkflowRunner.ActiveWorkflowRunner | undefined ;
2019-08-04 05:24:48 -07:00
let processExistCode = 0 ;
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
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 : Error ) = > {
console . log ( ` \ nWas not able to open URL in browser. Please open manually by visiting: \ n ${ editorUrl } \ n ` ) ;
} ) ;
}
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
/ * *
* Stoppes the n8n in a graceful way .
* Make for example sure that all the webhooks from third party services
* get removed .
* /
static async stopProcess() {
2021-05-01 20:43:01 -07:00
getLogger ( ) . info ( '\nStopping n8n...' ) ;
2019-06-23 03:35:23 -07:00
2020-10-14 07:38:48 -07:00
try {
2020-10-14 07:41:42 -07:00
const externalHooks = ExternalHooks ( ) ;
2020-10-14 07:38:48 -07:00
await externalHooks . run ( 'n8n.stop' , [ ] ) ;
2019-06-23 03:35:23 -07:00
2020-10-14 07:41:42 -07:00
setTimeout ( ( ) = > {
// In case that something goes wrong with shutdown we
// kill after max. 30 seconds no matter what
process . exit ( processExistCode ) ;
} , 30000 ) ;
2019-06-23 03:35:23 -07:00
2021-02-09 14:32:40 -08:00
const skipWebhookDeregistration = config . get ( 'endpoints.skipWebhoooksDeregistrationOnShutdown' ) as boolean ;
2020-10-14 07:41:42 -07:00
const removePromises = [ ] ;
2021-02-09 14:32:40 -08:00
if ( activeWorkflowRunner !== undefined && skipWebhookDeregistration !== true ) {
2020-10-14 07:41:42 -07:00
removePromises . push ( activeWorkflowRunner . removeAll ( ) ) ;
}
2020-09-14 03:30:58 -07:00
2020-10-14 07:41:42 -07:00
// Remove all test webhooks
const testWebhooks = TestWebhooks . getInstance ( ) ;
removePromises . push ( testWebhooks . removeAll ( ) ) ;
await Promise . all ( removePromises ) ;
// Wait for active workflow executions to finish
const activeExecutionsInstance = ActiveExecutions . getInstance ( ) ;
2021-03-25 03:23:54 -07:00
let executingWorkflows = activeExecutionsInstance . getActiveExecutions ( ) as IExecutionsCurrentSummary [ ] ;
2020-10-14 07:41:42 -07:00
let count = 0 ;
while ( executingWorkflows . length !== 0 ) {
if ( count ++ % 4 === 0 ) {
console . log ( ` Waiting for ${ executingWorkflows . length } active executions to finish... ` ) ;
2021-03-12 02:35:23 -08:00
executingWorkflows . map ( execution = > {
console . log ( ` - Execution ID ${ execution . id } , workflow ID: ${ execution . workflowId } ` ) ;
} ) ;
2020-10-14 07:41:42 -07:00
}
await new Promise ( ( resolve ) = > {
setTimeout ( resolve , 500 ) ;
} ) ;
executingWorkflows = activeExecutionsInstance . getActiveExecutions ( ) ;
2020-09-14 03:30:58 -07:00
}
2020-10-14 07:41:42 -07:00
} catch ( error ) {
console . error ( 'There was an error shutting down n8n.' , error ) ;
2020-09-14 03:30:58 -07:00
}
2019-08-28 06:28:47 -07:00
process . exit ( processExistCode ) ;
}
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
async run() {
// Make sure that n8n shuts down gracefully if possible
process . on ( 'SIGTERM' , Start . stopProcess ) ;
process . on ( 'SIGINT' , Start . stopProcess ) ;
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
const { flags } = this . parse ( Start ) ;
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
// Wrap that the process does not close but we can still use async
2020-12-24 00:06:43 -08:00
await ( async ( ) = > {
2019-08-28 06:28:47 -07:00
try {
2021-05-01 20:43:01 -07:00
const logger = getLogger ( ) ;
LoggerProxy . init ( logger ) ;
logger . info ( 'Initializing n8n process' ) ;
2021-07-22 01:22:17 -07:00
// todo remove a few versions after release
logger . info ( '\nn8n now checks for new versions and security updates. You can turn this off using the environment variable N8N_VERSION_NOTIFICATIONS_ENABLED to "false"\nFor more information, please refer to https://docs.n8n.io/getting-started/installation/advanced/configuration.html\n' ) ;
2019-08-28 06:28:47 -07:00
// Start directly with the init of the database to improve startup time
2021-03-25 03:23:54 -07:00
const startDbInitPromise = Db . init ( ) . catch ( ( error : Error ) = > {
2021-05-01 20:43:01 -07:00
logger . error ( ` There was an error initializing DB: " ${ error . message } " ` ) ;
2020-12-24 00:06:43 -08:00
processExistCode = 1 ;
// @ts-ignore
process . emit ( 'SIGINT' ) ;
2021-05-01 20:43:01 -07:00
process . exit ( 1 ) ;
2020-12-24 00:06:43 -08:00
} ) ;
2019-08-28 06:28:47 -07:00
// Make sure the settings exist
const userSettings = await UserSettings . prepareUserSettings ( ) ;
// Load all node and credential types
const loadNodesAndCredentials = LoadNodesAndCredentials ( ) ;
await loadNodesAndCredentials . init ( ) ;
2020-01-25 23:48:38 -08:00
// Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites ( ) ;
await credentialsOverwrites . init ( ) ;
2020-05-03 23:56:01 -07:00
// Load all external hooks
const externalHooks = ExternalHooks ( ) ;
await externalHooks . init ( ) ;
2019-08-28 06:28:47 -07:00
// Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes ( ) ;
await nodeTypes . init ( loadNodesAndCredentials . nodeTypes ) ;
const credentialTypes = CredentialTypes ( ) ;
await credentialTypes . init ( loadNodesAndCredentials . credentialTypes ) ;
// Wait till the database is ready
await startDbInitPromise ;
2021-02-08 23:59:32 -08:00
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 , cumulativeTimeout = 0 ;
2021-03-25 03:23:54 -07:00
2021-02-08 23:59:32 -08:00
const settings = {
retryStrategy : ( times : number ) : number | null = > {
const now = Date . now ( ) ;
if ( now - lastTimer > 30000 ) {
// Means we had no timeout at all or last timeout was temporary and we recovered
lastTimer = now ;
cumulativeTimeout = 0 ;
} else {
cumulativeTimeout += now - lastTimer ;
lastTimer = now ;
if ( cumulativeTimeout > redisConnectionTimeoutLimit ) {
2021-05-01 20:43:01 -07:00
logger . error ( 'Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process." ) ;
2021-02-08 23:59:32 -08:00
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 ;
}
2021-03-25 03:23:54 -07:00
2021-02-08 23:59:32 -08:00
// This connection is going to be our heartbeat
// IORedis automatically pings redis and tries to reconnect
// We will be using the retryStrategy above
// to control how and when to exit.
const redis = new Redis ( settings ) ;
redis . on ( 'error' , ( error ) = > {
if ( error . toString ( ) . includes ( 'ECONNREFUSED' ) === true ) {
2021-05-01 20:43:01 -07:00
logger . warn ( 'Redis unavailable - trying to reconnect...' ) ;
2021-02-08 23:59:32 -08:00
} else {
2021-05-01 20:43:01 -07:00
logger . warn ( 'Error with Redis: ' , error ) ;
2021-02-08 23:59:32 -08:00
}
} ) ;
}
2021-03-25 03:23:54 -07:00
2020-12-31 01:42:16 -08:00
const dbType = await GenericHelpers . getConfigValue ( 'database.type' ) as DatabaseType ;
if ( dbType === 'sqlite' ) {
2021-01-04 00:30:02 -08:00
const shouldRunVacuum = config . get ( 'database.sqlite.executeVacuumOnStartup' ) as number ;
if ( shouldRunVacuum ) {
2021-08-27 09:22:57 -07:00
await Db . collections . Execution ! . query ( 'VACUUM;' ) ;
2020-12-31 01:42:16 -08:00
}
}
2019-08-28 06:28:47 -07:00
if ( flags . tunnel === true ) {
this . log ( '\nWaiting for tunnel ...' ) ;
2019-10-14 22:21:15 -07:00
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 ) {
2019-08-28 06:28:47 -07:00
// When no tunnel subdomain did exist yet create a new random one
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789' ;
userSettings . tunnelSubdomain = Array . from ( { length : 24 } ) . map ( ( ) = > {
return availableCharacters . charAt ( Math . floor ( Math . random ( ) * availableCharacters . length ) ) ;
} ) . join ( '' ) ;
await UserSettings . writeUserSettings ( userSettings ) ;
}
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
const tunnelSettings : localtunnel.TunnelConfig = {
host : 'https://hooks.n8n.cloud' ,
2019-10-14 22:21:15 -07:00
subdomain : tunnelSubdomain ,
2019-08-28 06:28:47 -07:00
} ;
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
const port = config . get ( 'port' ) as number ;
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
// @ts-ignore
2019-12-29 10:22:37 -08:00
const webhookTunnel = await localtunnel ( port , tunnelSettings ) ;
2019-06-23 03:35:23 -07:00
2021-02-09 14:32:40 -08:00
process . env . WEBHOOK_URL = webhookTunnel . url + '/' ;
this . log ( ` Tunnel URL: ${ process . env . WEBHOOK_URL } \ n ` ) ;
2019-08-28 06:28:47 -07:00
this . log ( 'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!' ) ;
}
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
await Server . start ( ) ;
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
// Start to get active workflows and run their triggers
activeWorkflowRunner = ActiveWorkflowRunner . getInstance ( ) ;
await activeWorkflowRunner . init ( ) ;
2019-06-23 03:35:23 -07:00
2021-08-21 05:11:32 -07:00
const waitTracker = WaitTracker ( ) ;
2019-08-28 06:28:47 -07:00
const editorUrl = GenericHelpers . getBaseUrl ( ) ;
this . log ( ` \ nEditor is now accessible via: \ n ${ editorUrl } ` ) ;
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
// Allow to open n8n editor by pressing "o"
if ( Boolean ( process . stdout . isTTY ) && process . stdin . setRawMode ) {
process . stdin . setRawMode ( true ) ;
process . stdin . resume ( ) ;
process . stdin . setEncoding ( 'utf8' ) ;
let inputText = '' ;
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
if ( flags . open === true ) {
Start . openBrowser ( ) ;
}
this . log ( ` \ nPress "o" to open in Browser. ` ) ;
2021-03-25 03:23:54 -07:00
process . stdin . on ( 'data' , ( key : string ) = > {
2019-08-28 06:28:47 -07:00
if ( key === 'o' ) {
Start . openBrowser ( ) ;
inputText = '' ;
} else if ( key . charCodeAt ( 0 ) === 3 ) {
// Ctrl + c got pressed
Start . stopProcess ( ) ;
} else {
// When anything else got pressed, record it and send it on enter into the child process
if ( key . charCodeAt ( 0 ) === 13 ) {
// send to child process and print in terminal
process . stdout . write ( '\n' ) ;
2019-06-23 03:35:23 -07:00
inputText = '' ;
} else {
2019-08-28 06:28:47 -07:00
// record it and write into terminal
inputText += key ;
process . stdout . write ( key ) ;
2019-06-23 03:35:23 -07:00
}
2019-08-28 06:28:47 -07:00
}
} ) ;
2019-06-23 03:35:23 -07:00
}
2019-08-28 06:28:47 -07:00
} catch ( error ) {
this . error ( ` There was an error: ${ error . message } ` ) ;
2019-06-23 03:35:23 -07:00
2019-08-28 06:28:47 -07:00
processExistCode = 1 ;
// @ts-ignore
process . emit ( 'SIGINT' ) ;
}
} ) ( ) ;
}
}