2019-08-08 11:38:25 -07:00
import {
2020-05-14 05:27:19 -07:00
CredentialsOverwrites ,
CredentialTypes ,
2021-02-08 23:59:32 -08:00
Db ,
2020-11-13 14:31:27 -08:00
ExternalHooks ,
2021-04-17 07:44:07 -07:00
IWorkflowExecuteProcess ,
2019-08-08 11:38:25 -07:00
IWorkflowExecutionDataProcessWithExecution ,
NodeTypes ,
WorkflowExecuteAdditionalData ,
2021-02-20 04:51:06 -08:00
WorkflowHelpers ,
2019-08-08 11:38:25 -07:00
} from './' ;
import {
IProcessMessage ,
WorkflowExecute ,
} from 'n8n-core' ;
import {
2021-04-16 09:33:36 -07:00
ExecutionError ,
2019-08-08 11:38:25 -07:00
IDataObject ,
2021-02-20 04:51:06 -08:00
IExecuteWorkflowInfo ,
2021-05-01 20:43:01 -07:00
ILogger ,
2021-02-20 04:51:06 -08:00
INodeExecutionData ,
2019-08-08 11:38:25 -07:00
INodeType ,
INodeTypeData ,
IRun ,
ITaskData ,
2021-02-20 04:51:06 -08:00
IWorkflowExecuteAdditionalData ,
2020-11-13 14:31:27 -08:00
IWorkflowExecuteHooks ,
2021-05-01 20:43:01 -07:00
LoggerProxy ,
2019-08-08 11:38:25 -07:00
Workflow ,
2021-07-10 02:34:41 -07:00
WorkflowExecuteMode ,
2019-12-19 14:07:55 -08:00
WorkflowHooks ,
2021-04-16 09:33:36 -07:00
WorkflowOperationError ,
2019-08-08 11:38:25 -07:00
} from 'n8n-workflow' ;
2021-05-01 20:43:01 -07:00
import {
getLogger ,
} from '../src/Logger' ;
2021-06-23 02:20:07 -07:00
import * as config from '../config' ;
2021-02-08 23:59:32 -08:00
2019-08-08 11:38:25 -07:00
export class WorkflowRunnerProcess {
data : IWorkflowExecutionDataProcessWithExecution | undefined ;
2021-05-01 20:43:01 -07:00
logger : ILogger ;
2019-08-08 11:38:25 -07:00
startedAt = new Date ( ) ;
workflow : Workflow | undefined ;
workflowExecute : WorkflowExecute | undefined ;
2021-02-20 04:51:06 -08:00
executionIdCallback : ( executionId : string ) = > void | undefined ;
2021-04-17 07:44:07 -07:00
childExecutions : {
[ key : string ] : IWorkflowExecuteProcess ,
} = { } ;
2019-08-08 11:38:25 -07:00
2021-03-10 06:51:18 -08:00
static async stopProcess() {
setTimeout ( ( ) = > {
// Attempt a graceful shutdown, giving executions 30 seconds to finish
process . exit ( 0 ) ;
} , 30000 ) ;
}
2019-08-09 12:05:15 -07:00
2019-08-08 11:38:25 -07:00
async runWorkflow ( inputData : IWorkflowExecutionDataProcessWithExecution ) : Promise < IRun > {
2021-03-10 06:51:18 -08:00
process . on ( 'SIGTERM' , WorkflowRunnerProcess . stopProcess ) ;
process . on ( 'SIGINT' , WorkflowRunnerProcess . stopProcess ) ;
2021-05-01 20:43:01 -07:00
const logger = this . logger = getLogger ( ) ;
LoggerProxy . init ( logger ) ;
2019-08-08 11:38:25 -07:00
this . data = inputData ;
2021-05-01 20:43:01 -07:00
logger . verbose ( 'Initializing n8n sub-process' , { pid : process.pid , workflowId : this.data.workflowData.id } ) ;
2019-08-08 11:38:25 -07:00
let className : string ;
let tempNode : INodeType ;
let filePath : string ;
this . startedAt = new Date ( ) ;
const nodeTypesData : INodeTypeData = { } ;
for ( const nodeTypeName of Object . keys ( this . data . nodeTypeData ) ) {
className = this . data . nodeTypeData [ nodeTypeName ] . className ;
filePath = this . data . nodeTypeData [ nodeTypeName ] . sourcePath ;
const tempModule = require ( filePath ) ;
try {
tempNode = new tempModule [ className ] ( ) as INodeType ;
} catch ( error ) {
throw new Error ( ` Error loading node " ${ nodeTypeName } " from: " ${ filePath } " ` ) ;
}
nodeTypesData [ nodeTypeName ] = {
type : tempNode ,
sourcePath : filePath ,
} ;
}
const nodeTypes = NodeTypes ( ) ;
await nodeTypes . init ( nodeTypesData ) ;
2020-05-14 05:27:19 -07:00
// Init credential types the workflow uses (is needed to apply default values to credentials)
const credentialTypes = CredentialTypes ( ) ;
await credentialTypes . init ( inputData . credentialsTypeData ) ;
// Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites ( ) ;
2020-09-12 12:13:57 -07:00
await credentialsOverwrites . init ( inputData . credentialsOverwrite ) ;
2020-05-14 05:27:19 -07:00
2020-11-13 14:31:27 -08:00
// Load all external hooks
const externalHooks = ExternalHooks ( ) ;
await externalHooks . init ( ) ;
2021-02-08 23:59:32 -08:00
// This code has been split into 3 ifs just to make it easier to understand
// Can be made smaller but in the end it will make it impossible to read.
if ( inputData . workflowData . settings !== undefined && inputData . workflowData . settings . saveExecutionProgress === true ) {
// Workflow settings specifying it should save
await Db . init ( ) ;
} else if ( inputData . workflowData . settings !== undefined && inputData . workflowData . settings . saveExecutionProgress !== false && config . get ( 'executions.saveExecutionProgress' ) as boolean ) {
// Workflow settings not saying anything about saving but default settings says so
await Db . init ( ) ;
} else if ( inputData . workflowData . settings === undefined && config . get ( 'executions.saveExecutionProgress' ) as boolean ) {
// Workflow settings not saying anything about saving but default settings says so
await Db . init ( ) ;
}
2021-04-17 07:44:07 -07:00
// Start timeout for the execution
let workflowTimeout = config . get ( 'executions.timeout' ) as number ; // initialize with default
if ( this . data . workflowData . settings && this . data . workflowData . settings . executionTimeout ) {
workflowTimeout = this . data . workflowData . settings ! . executionTimeout as number ; // preference on workflow setting
}
if ( workflowTimeout > 0 ) {
workflowTimeout = Math . min ( workflowTimeout , config . get ( 'executions.maxTimeout' ) as number ) ;
}
2021-02-20 04:51:06 -08:00
this . workflow = new Workflow ( { id : this.data.workflowData.id as string | undefined , name : this.data.workflowData.name , nodes : this.data.workflowData ! . nodes , connections : this.data.workflowData ! . connections , active : this.data.workflowData ! . active , nodeTypes , staticData : this.data.workflowData ! . staticData , settings : this.data.workflowData ! . settings } ) ;
2021-04-17 07:44:07 -07:00
const additionalData = await WorkflowExecuteAdditionalData . getBase ( this . data . credentials , undefined , workflowTimeout <= 0 ? undefined : Date . now ( ) + workflowTimeout * 1000 ) ;
2019-08-08 11:38:25 -07:00
additionalData . hooks = this . getProcessForwardHooks ( ) ;
2021-06-12 11:22:55 -07:00
additionalData . sendMessageToUI = async ( source : string , message : any ) = > { // tslint:disable-line:no-any
2021-05-29 11:41:25 -07:00
if ( workflowRunner . data ! . executionMode !== 'manual' ) {
return ;
}
try {
await sendToParentProcess ( 'sendMessageToUI' , { source , message } ) ;
} catch ( error ) {
this . logger . error ( ` There was a problem sending UI data to parent process: " ${ error . message } " ` ) ;
}
} ;
2021-02-20 04:51:06 -08:00
const executeWorkflowFunction = additionalData . executeWorkflow ;
additionalData . executeWorkflow = async ( workflowInfo : IExecuteWorkflowInfo , additionalData : IWorkflowExecuteAdditionalData , inputData? : INodeExecutionData [ ] | undefined ) : Promise < Array < INodeExecutionData [ ] | null > | IRun > = > {
const workflowData = await WorkflowExecuteAdditionalData . getWorkflowData ( workflowInfo ) ;
const runData = await WorkflowExecuteAdditionalData . getRunData ( workflowData , inputData ) ;
await sendToParentProcess ( 'startExecution' , { runData } ) ;
const executionId : string = await new Promise ( ( resolve ) = > {
this . executionIdCallback = ( executionId : string ) = > {
resolve ( executionId ) ;
} ;
} ) ;
2021-03-18 15:24:57 -07:00
let result : IRun ;
try {
2021-04-17 07:44:07 -07:00
const executeWorkflowFunctionOutput = await executeWorkflowFunction ( workflowInfo , additionalData , inputData , executionId , workflowData , runData ) as { workflowExecute : WorkflowExecute , workflow : Workflow } as IWorkflowExecuteProcess ;
const workflowExecute = executeWorkflowFunctionOutput . workflowExecute ;
this . childExecutions [ executionId ] = executeWorkflowFunctionOutput ;
const workflow = executeWorkflowFunctionOutput . workflow ;
result = await workflowExecute . processRunExecutionData ( workflow ) as IRun ;
await externalHooks . run ( 'workflow.postExecute' , [ result , workflowData ] ) ;
await sendToParentProcess ( 'finishExecution' , { executionId , result } ) ;
delete this . childExecutions [ executionId ] ;
2021-03-18 15:24:57 -07:00
} catch ( e ) {
await sendToParentProcess ( 'finishExecution' , { executionId } ) ;
2021-04-17 07:44:07 -07:00
delete this . childExecutions [ executionId ] ;
// Throw same error we had
throw e ;
2021-03-18 15:24:57 -07:00
}
2021-02-20 04:51:06 -08:00
2021-05-01 20:43:01 -07:00
await sendToParentProcess ( 'finishExecution' , { executionId , result } ) ;
2021-02-20 04:51:06 -08:00
const returnData = WorkflowHelpers . getDataLastExecutedNodeData ( result ) ;
return returnData ! . data ! . main ;
} ;
2019-08-08 11:38:25 -07:00
if ( this . data . executionData !== undefined ) {
this . workflowExecute = new WorkflowExecute ( additionalData , this . data . executionMode , this . data . executionData ) ;
return this . workflowExecute . processRunExecutionData ( this . workflow ) ;
} else if ( this . data . runData === undefined || this . data . startNodes === undefined || this . data . startNodes . length === 0 || this . data . destinationNode === undefined ) {
// Execute all nodes
2021-02-20 04:51:06 -08:00
2019-08-08 11:38:25 -07:00
// Can execute without webhook so go on
this . workflowExecute = new WorkflowExecute ( additionalData , this . data . executionMode ) ;
return this . workflowExecute . run ( this . workflow , undefined , this . data . destinationNode ) ;
} else {
// Execute only the nodes between start and destination nodes
this . workflowExecute = new WorkflowExecute ( additionalData , this . data . executionMode ) ;
return this . workflowExecute . runPartialWorkflow ( this . workflow , this . data . runData , this . data . startNodes , this . data . destinationNode ) ;
}
}
2019-08-09 12:05:15 -07:00
/ * *
* Sends hook data to the parent process that it executes them
*
* @param { string } hook
* @param { any [ ] } parameters
* @memberof WorkflowRunnerProcess
* /
2021-05-11 20:15:22 -07:00
async sendHookToParentProcess ( hook : string , parameters : any [ ] ) { // tslint:disable-line:no-any
2019-08-09 12:05:15 -07:00
try {
2021-05-11 20:15:22 -07:00
await sendToParentProcess ( 'processHook' , {
2019-08-08 11:38:25 -07:00
hook ,
parameters ,
2019-08-09 12:05:15 -07:00
} ) ;
} catch ( error ) {
2021-05-01 20:43:01 -07:00
this . logger . error ( ` There was a problem sending hook: " ${ hook } " ` , { parameters , error } ) ;
2019-08-09 12:05:15 -07:00
}
2019-08-08 11:38:25 -07:00
}
2019-08-09 12:05:15 -07:00
2019-08-08 11:38:25 -07:00
/ * *
* Create a wrapper for hooks which simply forwards the data to
* the parent process where they then can be executed with access
* to database and to PushService
*
* @returns
* /
2019-12-19 14:07:55 -08:00
getProcessForwardHooks ( ) : WorkflowHooks {
2020-11-13 14:31:27 -08:00
const hookFunctions : IWorkflowExecuteHooks = {
2019-08-08 11:38:25 -07:00
nodeExecuteBefore : [
async ( nodeName : string ) : Promise < void > = > {
2021-05-11 20:15:22 -07:00
await this . sendHookToParentProcess ( 'nodeExecuteBefore' , [ nodeName ] ) ;
2019-08-08 11:38:25 -07:00
} ,
] ,
nodeExecuteAfter : [
2021-03-10 01:50:07 -08:00
async ( nodeName : string , data : ITaskData ) : Promise < void > = > {
2021-05-11 20:15:22 -07:00
await this . sendHookToParentProcess ( 'nodeExecuteAfter' , [ nodeName , data ] ) ;
2019-08-08 11:38:25 -07:00
} ,
] ,
workflowExecuteBefore : [
async ( ) : Promise < void > = > {
2021-05-11 20:15:22 -07:00
await this . sendHookToParentProcess ( 'workflowExecuteBefore' , [ ] ) ;
2020-10-22 06:46:03 -07:00
} ,
2019-08-08 11:38:25 -07:00
] ,
workflowExecuteAfter : [
async ( fullRunData : IRun , newStaticData? : IDataObject ) : Promise < void > = > {
2021-05-11 20:15:22 -07:00
await this . sendHookToParentProcess ( 'workflowExecuteAfter' , [ fullRunData , newStaticData ] ) ;
2019-08-08 11:38:25 -07:00
} ,
2020-10-22 06:46:03 -07:00
] ,
2019-08-08 11:38:25 -07:00
} ;
2019-12-19 14:07:55 -08:00
2020-11-13 14:31:27 -08:00
const preExecuteFunctions = WorkflowExecuteAdditionalData . hookFunctionsPreExecute ( ) ;
for ( const key of Object . keys ( preExecuteFunctions ) ) {
2021-02-08 23:59:32 -08:00
if ( hookFunctions [ key ] === undefined ) {
hookFunctions [ key ] = [ ] ;
}
2020-11-13 14:31:27 -08:00
hookFunctions [ key ] ! . push . apply ( hookFunctions [ key ] , preExecuteFunctions [ key ] ) ;
}
2019-12-19 14:07:55 -08:00
return new WorkflowHooks ( hookFunctions , this . data ! . executionMode , this . data ! . executionId , this . data ! . workflowData , { sessionId : this.data ! . sessionId , retryOf : this.data ! . retryOf as string } ) ;
2019-08-08 11:38:25 -07:00
}
}
/ * *
* Sends data to parent process
*
* @param { string } type The type of data to send
* @param { * } data The data
2019-08-09 02:07:54 -07:00
* @returns { Promise < void > }
2019-08-08 11:38:25 -07:00
* /
2019-08-09 02:07:54 -07:00
async function sendToParentProcess ( type : string , data : any ) : Promise < void > { // tslint:disable-line:no-any
return new Promise ( ( resolve , reject ) = > {
process . send ! ( {
type ,
data ,
} , ( error : Error ) = > {
if ( error ) {
return reject ( error ) ;
}
resolve ( ) ;
} ) ;
2019-08-08 11:38:25 -07:00
} ) ;
}
const workflowRunner = new WorkflowRunnerProcess ( ) ;
// Listen to messages from parent process which send the data of
// the worflow to process
process . on ( 'message' , async ( message : IProcessMessage ) = > {
try {
if ( message . type === 'startWorkflow' ) {
2021-04-17 07:44:07 -07:00
await sendToParentProcess ( 'start' , { } ) ;
2019-08-08 11:38:25 -07:00
const runData = await workflowRunner . runWorkflow ( message . data ) ;
2019-08-09 02:07:54 -07:00
await sendToParentProcess ( 'end' , {
2019-08-08 11:38:25 -07:00
runData ,
} ) ;
// Once the workflow got executed make sure the process gets killed again
process . exit ( ) ;
2020-07-29 05:12:54 -07:00
} else if ( message . type === 'stopExecution' || message . type === 'timeout' ) {
2019-08-08 11:38:25 -07:00
// The workflow execution should be stopped
2019-08-09 12:05:15 -07:00
let runData : IRun ;
2019-08-08 11:38:25 -07:00
if ( workflowRunner . workflowExecute !== undefined ) {
2021-04-17 07:44:07 -07:00
const executionIds = Object . keys ( workflowRunner . childExecutions ) ;
for ( const executionId of executionIds ) {
const childWorkflowExecute = workflowRunner . childExecutions [ executionId ] ;
runData = childWorkflowExecute . workflowExecute . getFullRunData ( workflowRunner . childExecutions [ executionId ] . startedAt ) ;
2021-07-10 02:34:41 -07:00
const timeOutError = message . type === 'timeout' ? new WorkflowOperationError ( 'Workflow execution timed out!' ) : new WorkflowOperationError ( 'Workflow-Execution has been canceled!' ) ;
2021-04-17 07:44:07 -07:00
// If there is any data send it to parent process, if execution timedout add the error
await childWorkflowExecute . workflowExecute . processSuccessExecution ( workflowRunner . childExecutions [ executionId ] . startedAt , childWorkflowExecute . workflow , timeOutError ) ;
}
2019-08-08 11:38:25 -07:00
// Workflow started already executing
2019-08-09 12:05:15 -07:00
runData = workflowRunner . workflowExecute . getFullRunData ( workflowRunner . startedAt ) ;
2019-08-08 11:38:25 -07:00
2021-07-10 02:34:41 -07:00
const timeOutError = message . type === 'timeout' ? new WorkflowOperationError ( 'Workflow execution timed out!' ) : new WorkflowOperationError ( 'Workflow-Execution has been canceled!' ) ;
2020-07-29 05:12:54 -07:00
// If there is any data send it to parent process, if execution timedout add the error
await workflowRunner . workflowExecute . processSuccessExecution ( workflowRunner . startedAt , workflowRunner . workflow ! , timeOutError ) ;
2019-08-08 11:38:25 -07:00
} else {
// Workflow did not get started yet
2019-08-09 12:05:15 -07:00
runData = {
2019-08-08 11:38:25 -07:00
data : {
resultData : {
runData : { } ,
} ,
} ,
2021-07-10 02:34:41 -07:00
finished : false ,
mode : workflowRunner.data ? workflowRunner . data ! . executionMode : 'own' as WorkflowExecuteMode ,
2019-08-08 11:38:25 -07:00
startedAt : workflowRunner.startedAt ,
stoppedAt : new Date ( ) ,
} ;
2019-08-09 12:05:15 -07:00
workflowRunner . sendHookToParentProcess ( 'workflowExecuteAfter' , [ runData ] ) ;
2019-08-08 11:38:25 -07:00
}
2020-07-29 05:12:54 -07:00
await sendToParentProcess ( message . type === 'timeout' ? message . type : 'end' , {
2019-08-09 12:05:15 -07:00
runData ,
2019-08-08 11:38:25 -07:00
} ) ;
// Stop process
process . exit ( ) ;
2021-02-20 04:51:06 -08:00
} else if ( message . type === 'executionId' ) {
workflowRunner . executionIdCallback ( message . data . executionId ) ;
2019-08-08 11:38:25 -07:00
}
} catch ( error ) {
2021-04-16 09:33:36 -07:00
2019-08-08 11:38:25 -07:00
// Catch all uncaught errors and forward them to parent process
const executionError = {
2021-04-16 09:33:36 -07:00
. . . error ,
name : error ! . name || 'Error' ,
message : error ! . message ,
stack : error ! . stack ,
} as ExecutionError ;
2019-08-08 11:38:25 -07:00
2019-08-09 02:07:54 -07:00
await sendToParentProcess ( 'processError' , {
2019-08-08 11:38:25 -07:00
executionError ,
} ) ;
process . exit ( ) ;
}
2019-08-09 02:07:54 -07:00
} ) ;