Run workflows in own independent subprocess

This commit is contained in:
Jan Oberhauser 2019-08-08 20:38:25 +02:00
parent abb0a52b08
commit d59a043e3f
21 changed files with 926 additions and 369 deletions

View file

@ -2,24 +2,20 @@ import Vorpal = require('vorpal');
import { Args } from 'vorpal'; import { Args } from 'vorpal';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { import {
CredentialTypes, ActiveExecutions,
Db, Db,
GenericHelpers,
IWorkflowBase, IWorkflowBase,
IWorkflowExecutionDataProcess,
LoadNodesAndCredentials, LoadNodesAndCredentials,
NodeTypes, NodeTypes,
GenericHelpers, WorkflowCredentials,
WorkflowHelpers, WorkflowHelpers,
WorkflowExecuteAdditionalData, WorkflowRunner,
} from "../src"; } from "../src";
import { import {
ActiveExecutions,
UserSettings, UserSettings,
WorkflowExecute,
} from "n8n-core"; } from "n8n-core";
import {
INode,
Workflow,
} from "n8n-workflow";
module.exports = (vorpal: Vorpal) => { module.exports = (vorpal: Vorpal) => {
@ -99,22 +95,16 @@ module.exports = (vorpal: Vorpal) => {
// Add the found types to an instance other parts of the application can use // Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
await nodeTypes.init(loadNodesAndCredentials.nodeTypes); await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
const credentialTypes = CredentialTypes();
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) { if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) {
workflowId = undefined; workflowId = undefined;
} }
const workflowInstance = new Workflow(workflowId, workflowData!.nodes, workflowData!.connections, true, nodeTypes, workflowData!.staticData);
// Check if the workflow contains the required "Start" node // Check if the workflow contains the required "Start" node
// "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue // "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue
const requiredNodeTypes = ['n8n-nodes-base.start']; const requiredNodeTypes = ['n8n-nodes-base.start'];
let startNodeFound = false; let startNodeFound = false;
let node: INode; for (const node of workflowData!.nodes) {
for (const nodeName of Object.keys(workflowInstance.nodes)) {
node = workflowInstance.nodes[nodeName];
if (requiredNodeTypes.includes(node.type)) { if (requiredNodeTypes.includes(node.type)) {
startNodeFound = true; startNodeFound = true;
} }
@ -127,12 +117,17 @@ module.exports = (vorpal: Vorpal) => {
return Promise.resolve(); return Promise.resolve();
} }
const mode = 'cli';
const additionalData = await WorkflowExecuteAdditionalData.get(mode, workflowData!, workflowInstance);
const workflowExecute = new WorkflowExecute(additionalData, mode);
try { try {
const executionId = await workflowExecute.run(workflowInstance); const credentials = await WorkflowCredentials(workflowData!.nodes);
const runData: IWorkflowExecutionDataProcess = {
credentials,
executionMode: 'cli',
workflowData: workflowData!,
};
const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(runData);
const activeExecutions = ActiveExecutions.getInstance(); const activeExecutions = ActiveExecutions.getInstance();
const data = await activeExecutions.getPostExecutePromise(executionId); const data = await activeExecutions.getPostExecutePromise(executionId);

View file

@ -1,43 +1,42 @@
import { import {
IRun, IRun,
IRunExecutionData,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
createDeferredPromise, createDeferredPromise,
IExecutingWorkflowData,
IExecutionsCurrentSummary, IExecutionsCurrentSummary,
} from 'n8n-core';
import {
IExecutingWorkflowData,
IWorkflowExecutionDataProcess,
} from '.'; } from '.';
import { ChildProcess } from 'child_process';
export class ActiveExecutions { export class ActiveExecutions {
private nextId = 1; private nextId = 1;
private activeExecutions: { private activeExecutions: {
[index: string]: IExecutingWorkflowData; [index: string]: IExecutingWorkflowData;
} = {}; } = {};
private stopExecutions: string[] = [];
/** /**
* Add a new active execution * Add a new active execution
* *
* @param {Workflow} workflow * @param {ChildProcess} process
* @param {IRunExecutionData} runExecutionData * @param {IWorkflowExecutionDataProcess} executionData
* @param {WorkflowExecuteMode} mode
* @returns {string} * @returns {string}
* @memberof ActiveExecutions * @memberof ActiveExecutions
*/ */
add(workflow: Workflow, runExecutionData: IRunExecutionData, mode: WorkflowExecuteMode): string { add(process: ChildProcess, executionData: IWorkflowExecutionDataProcess): string {
const executionId = this.nextId++; const executionId = this.nextId++;
this.activeExecutions[executionId] = { this.activeExecutions[executionId] = {
runExecutionData, executionData,
process,
startedAt: new Date(), startedAt: new Date(),
mode,
workflow,
postExecutePromises: [], postExecutePromises: [],
}; };
@ -53,7 +52,7 @@ export class ActiveExecutions {
* @returns {void} * @returns {void}
* @memberof ActiveExecutions * @memberof ActiveExecutions
*/ */
remove(executionId: string, fullRunData: IRun): void { remove(executionId: string, fullRunData?: IRun): void {
if (this.activeExecutions[executionId] === undefined) { if (this.activeExecutions[executionId] === undefined) {
return; return;
} }
@ -65,12 +64,6 @@ export class ActiveExecutions {
// Remove from the list of active executions // Remove from the list of active executions
delete this.activeExecutions[executionId]; delete this.activeExecutions[executionId];
const stopExecutionIndex = this.stopExecutions.indexOf(executionId);
if (stopExecutionIndex !== -1) {
// If it was on the stop-execution list remove it
this.stopExecutions.splice(stopExecutionIndex, 1);
}
} }
@ -87,16 +80,20 @@ export class ActiveExecutions {
return; return;
} }
if (!this.stopExecutions.includes(executionId)) { // In case something goes wrong make sure that promise gets first
// Add the execution to the stop list if it is not already on it // returned that it gets then also resolved correctly.
this.stopExecutions.push(executionId); setTimeout(() => {
} if (this.activeExecutions[executionId].process.connected) {
this.activeExecutions[executionId].process.send({
type: 'stopExecution'
});
}
}, 1);
return this.getPostExecutePromise(executionId); return this.getPostExecutePromise(executionId);
} }
/** /**
* Returns a promise which will resolve with the data of the execution * Returns a promise which will resolve with the data of the execution
* with the given id * with the given id
@ -105,9 +102,9 @@ export class ActiveExecutions {
* @returns {Promise<IRun>} * @returns {Promise<IRun>}
* @memberof ActiveExecutions * @memberof ActiveExecutions
*/ */
async getPostExecutePromise(executionId: string): Promise<IRun> { async getPostExecutePromise(executionId: string): Promise<IRun | undefined> {
// Create the promise which will be resolved when the execution finished // Create the promise which will be resolved when the execution finished
const waitPromise = await createDeferredPromise<IRun>(); const waitPromise = await createDeferredPromise<IRun | undefined>();
if (this.activeExecutions[executionId] === undefined) { if (this.activeExecutions[executionId] === undefined) {
throw new Error(`There is no active execution with id "${executionId}".`); throw new Error(`There is no active execution with id "${executionId}".`);
@ -119,20 +116,6 @@ export class ActiveExecutions {
} }
/**
* Returns if the execution should be stopped
*
* @param {string} executionId The execution id to check
* @returns {boolean}
* @memberof ActiveExecutions
*/
shouldBeStopped(executionId: string): boolean {
return this.stopExecutions.includes(executionId);
}
/** /**
* Returns all the currently active executions * Returns all the currently active executions
* *
@ -142,15 +125,15 @@ export class ActiveExecutions {
getActiveExecutions(): IExecutionsCurrentSummary[] { getActiveExecutions(): IExecutionsCurrentSummary[] {
const returnData: IExecutionsCurrentSummary[] = []; const returnData: IExecutionsCurrentSummary[] = [];
let executionData; let data;
for (const id of Object.keys(this.activeExecutions)) { for (const id of Object.keys(this.activeExecutions)) {
executionData = this.activeExecutions[id]; data = this.activeExecutions[id];
returnData.push( returnData.push(
{ {
id, id,
startedAt: executionData.startedAt, startedAt: data.startedAt,
mode: executionData.mode, mode: data.executionData.executionMode,
workflowId: executionData.workflow.id!, workflowId: data.executionData.workflowData.id! as string,
} }
); );
} }

View file

@ -4,18 +4,28 @@ import {
NodeTypes, NodeTypes,
IResponseCallbackData, IResponseCallbackData,
IWorkflowDb, IWorkflowDb,
IWorkflowExecutionDataProcess,
ResponseHelper, ResponseHelper,
WebhookHelpers, WebhookHelpers,
WorkflowCredentials,
WorkflowHelpers, WorkflowHelpers,
WorkflowRunner,
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
} from './'; } from './';
import { import {
ActiveWorkflows, ActiveWorkflows,
ActiveWebhooks, ActiveWebhooks,
NodeExecuteFunctions,
WorkflowExecute,
} from 'n8n-core'; } from 'n8n-core';
import { import {
IExecuteData,
IGetExecuteTriggerFunctions,
INode,
INodeExecutionData,
IRunExecutionData,
IWebhookData, IWebhookData,
IWorkflowExecuteAdditionalData as IWorkflowExecuteAdditionalDataWorkflow, IWorkflowExecuteAdditionalData as IWorkflowExecuteAdditionalDataWorkflow,
WebhookHttpMethod, WebhookHttpMethod,
@ -209,6 +219,57 @@ export class ActiveWorkflowRunner {
} }
/**
* Return trigger function which gets the global functions from n8n-core
* and overwrites the emit to be able to start it in subprocess
*
* @param {IWorkflowDb} workflowData
* @param {IWorkflowExecuteAdditionalDataWorkflow} additionalData
* @param {WorkflowExecuteMode} mode
* @returns {IGetExecuteTriggerFunctions}
* @memberof ActiveWorkflowRunner
*/
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): IGetExecuteTriggerFunctions{
return ((workflow: Workflow, node: INode) => {
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode);
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
const nodeExecutionStack: IExecuteData[] = [
{
node,
data: {
main: data,
}
}
];
const executionData: IRunExecutionData = {
startData: {},
resultData: {
runData: {},
},
executionData: {
contextData: {},
nodeExecutionStack,
waitingExecution: {},
},
};
// Start the workflow
const runData: IWorkflowExecutionDataProcess = {
credentials: additionalData.credentials,
executionMode: mode,
executionData,
workflowData,
};
const workflowRunner = new WorkflowRunner();
workflowRunner.run(runData);
};
return returnFunctions;
});
}
/** /**
* Makes a workflow active * Makes a workflow active
* *
@ -240,12 +301,13 @@ export class ActiveWorkflowRunner {
} }
const mode = 'trigger'; const mode = 'trigger';
const additionalData = await WorkflowExecuteAdditionalData.get(mode, workflowData, workflowInstance); const credentials = await WorkflowCredentials(workflowData.nodes);
const additionalData = await WorkflowExecuteAdditionalData.getBase(mode, credentials);
const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode);
// Add the workflows which have webhooks defined // Add the workflows which have webhooks defined
await this.addWorkflowWebhooks(workflowInstance, additionalData, mode); await this.addWorkflowWebhooks(workflowInstance, additionalData, mode);
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, getTriggerFunctions);
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData);
if (this.activationErrors[workflowId] !== undefined) { if (this.activationErrors[workflowId] !== undefined) {
// If there were any activation errors delete them // If there were any activation errors delete them
@ -265,6 +327,8 @@ export class ActiveWorkflowRunner {
throw error; throw error;
} }
// If for example webhooks get created it sometimes has to save the
// id of them in the static data. So make sure that data gets persisted.
await WorkflowHelpers.saveStaticData(workflowInstance!); await WorkflowHelpers.saveStaticData(workflowInstance!);
} }

View file

@ -6,15 +6,22 @@ import {
IExecutionError, IExecutionError,
INode, INode,
IRun, IRun,
IRunData,
IRunExecutionData, IRunExecutionData,
ITaskData, ITaskData,
IWorkflowCredentials,
IWorkflowSettings, IWorkflowSettings,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import {
IDeferredPromise,
} from 'n8n-core';
import { ObjectID, Repository } from "typeorm"; import { ObjectID, Repository } from "typeorm";
import { ChildProcess } from 'child_process';
import { Url } from 'url'; import { Url } from 'url';
import { Request } from 'express'; import { Request } from 'express';
@ -171,6 +178,13 @@ export interface IExecutionDeleteFilter {
ids?: string[]; ids?: string[];
} }
export interface IExecutingWorkflowData {
executionData: IWorkflowExecutionDataProcess;
process: ChildProcess;
startedAt: Date;
postExecutePromises: Array<IDeferredPromise<IRun | undefined>>;
}
export interface IN8nConfig { export interface IN8nConfig {
database: IN8nConfigDatabase; database: IN8nConfigDatabase;
endpoints: IN8nConfigEndpoints; endpoints: IN8nConfigEndpoints;
@ -282,6 +296,14 @@ export interface IResponseCallbackData {
} }
export interface ITransferNodeTypes {
[key: string]: {
className: string;
sourcePath: string;
};
}
export interface IWorkflowErrorData { export interface IWorkflowErrorData {
[key: string]: IDataObject | string | number | IExecutionError; [key: string]: IDataObject | string | number | IExecutionError;
execution: { execution: {
@ -295,3 +317,25 @@ export interface IWorkflowErrorData {
name: string; name: string;
}; };
} }
export interface IProcessMessageDataHook {
hook: string;
parameters: any[]; // tslint:disable-line:no-any
}
export interface IWorkflowExecutionDataProcess {
credentials: IWorkflowCredentials;
destinationNode?: string;
executionMode: WorkflowExecuteMode;
executionData?: IRunExecutionData;
runData?: IRunData;
retryOf?: number | string | ObjectID;
sessionId?: string;
startNodes?: string[];
workflowData: IWorkflowBase;
}
export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess {
executionId: string;
nodeTypeData: ITransferNodeTypes;
}

View file

@ -5,6 +5,7 @@ import {
import { import {
ICredentialType, ICredentialType,
INodeType, INodeType,
INodeTypeData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
@ -25,9 +26,7 @@ const fsStatAsync = promisify(fsStat);
class LoadNodesAndCredentialsClass { class LoadNodesAndCredentialsClass {
nodeTypes: { nodeTypes: INodeTypeData = {};
[key: string]: INodeType
} = {};
credentialTypes: { credentialTypes: {
[key: string]: ICredentialType [key: string]: ICredentialType
@ -37,7 +36,7 @@ class LoadNodesAndCredentialsClass {
nodeModulesPath = ''; nodeModulesPath = '';
async init(directory?: string) { async init() {
// Get the path to the node-modules folder to be later able // Get the path to the node-modules folder to be later able
// to load the credentials and nodes // to load the credentials and nodes
const checkPaths = [ const checkPaths = [
@ -172,12 +171,15 @@ class LoadNodesAndCredentialsClass {
tempNode.description.icon = 'file:' + path.join(path.dirname(filePath), tempNode.description.icon.substr(5)); tempNode.description.icon = 'file:' + path.join(path.dirname(filePath), tempNode.description.icon.substr(5));
} }
// Check if the node should be skipped // Check if the node should be skiped
if (this.excludeNodes !== undefined && this.excludeNodes.includes(fullNodeName)) { if (this.excludeNodes !== undefined && this.excludeNodes.includes(fullNodeName)) {
return; return;
} }
this.nodeTypes[fullNodeName] = tempNode; this.nodeTypes[fullNodeName] = {
type: tempNode,
sourcePath: filePath,
};
} }

View file

@ -1,26 +1,25 @@
import { import {
INodeType, INodeType,
INodeTypes, INodeTypes,
INodeTypeData,
} from 'n8n-workflow'; } from 'n8n-workflow';
class NodeTypesClass implements INodeTypes { class NodeTypesClass implements INodeTypes {
nodeTypes: { nodeTypes: INodeTypeData = {};
[key: string]: INodeType
} = {};
async init(nodeTypes: {[key: string]: INodeType }): Promise<void> { async init(nodeTypes: INodeTypeData): Promise<void> {
this.nodeTypes = nodeTypes; this.nodeTypes = nodeTypes;
} }
getAll(): INodeType[] { getAll(): INodeType[] {
return Object.values(this.nodeTypes); return Object.values(this.nodeTypes).map((data) => data.type);
} }
getByName(nodeType: string): INodeType | undefined { getByName(nodeType: string): INodeType | undefined {
return this.nodeTypes[nodeType]; return this.nodeTypes[nodeType].type;
} }
} }

View file

@ -4,15 +4,16 @@ import * as history from 'connect-history-api-fallback';
import * as requestPromise from 'request-promise-native'; import * as requestPromise from 'request-promise-native';
import { import {
IActivationError, ActiveExecutions,
ActiveWorkflowRunner, ActiveWorkflowRunner,
CredentialTypes,
Db,
IActivationError,
ICustomRequest, ICustomRequest,
ICredentialsDb, ICredentialsDb,
ICredentialsDecryptedDb, ICredentialsDecryptedDb,
ICredentialsDecryptedResponse, ICredentialsDecryptedResponse,
ICredentialsResponse, ICredentialsResponse,
CredentialTypes,
Db,
IExecutionDeleteFilter, IExecutionDeleteFilter,
IExecutionFlatted, IExecutionFlatted,
IExecutionFlattedDb, IExecutionFlattedDb,
@ -25,22 +26,23 @@ import {
IWorkflowBase, IWorkflowBase,
IWorkflowShortResponse, IWorkflowShortResponse,
IWorkflowResponse, IWorkflowResponse,
IWorkflowExecutionDataProcess,
NodeTypes, NodeTypes,
Push, Push,
ResponseHelper, ResponseHelper,
TestWebhooks, TestWebhooks,
WorkflowCredentials,
WebhookHelpers, WebhookHelpers,
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
WorkflowHelpers, WorkflowHelpers,
WorkflowRunner,
GenericHelpers, GenericHelpers,
} from './'; } from './';
import { import {
ActiveExecutions,
Credentials, Credentials,
LoadNodeParameterOptions, LoadNodeParameterOptions,
UserSettings, UserSettings,
WorkflowExecute,
} from 'n8n-core'; } from 'n8n-core';
import { import {
@ -127,7 +129,7 @@ class App {
throw new Error('Basic auth is activated but no password got defined. Please set one!'); throw new Error('Basic auth is activated but no password got defined. Please set one!');
} }
const authIgnoreRegex = new RegExp(`^\/(rest|${this.endpointWebhook}|${this.endpointWebhookTest})\/.*$`) const authIgnoreRegex = new RegExp(`^\/(rest|${this.endpointWebhook}|${this.endpointWebhookTest})\/.*$`);
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.url.match(authIgnoreRegex)) { if (req.url.match(authIgnoreRegex)) {
return next(); return next();
@ -386,45 +388,45 @@ class App {
const runData: IRunData | undefined = req.body.runData; const runData: IRunData | undefined = req.body.runData;
const startNodes: string[] | undefined = req.body.startNodes; const startNodes: string[] | undefined = req.body.startNodes;
const destinationNode: string | undefined = req.body.destinationNode; const destinationNode: string | undefined = req.body.destinationNode;
const nodeTypes = NodeTypes();
const executionMode = 'manual'; const executionMode = 'manual';
const sessionId = GenericHelpers.getSessionId(req); const sessionId = GenericHelpers.getSessionId(req);
// Do not supply the saved static data! Tests always run with initially empty static data. // Check if workflow is saved as webhooks can only be tested with saved workflows.
// The reason is that it contains information like webhook-ids. If a workflow is currently // If that is the case check if any webhooks calls are present we have to wait for and
// active it would see its id and would so not create an own test-webhook. Additionally would // if that is the case wait till we receive it.
// it also delete the webhook at the service in the end. So that the active workflow would end if (WorkflowHelpers.isWorkflowIdValid(workflowData.id) === true && (runData === undefined || startNodes === undefined || startNodes.length === 0 || destinationNode === undefined)) {
// up without still being active but not receiving and webhook requests anymore as it does // Webhooks can only be tested with saved workflows
// not exist anymore. const credentials = await WorkflowCredentials(workflowData.nodes);
const workflowInstance = new Workflow(workflowData.id, workflowData.nodes, workflowData.connections, false, nodeTypes, undefined, workflowData.settings); const additionalData = await WorkflowExecuteAdditionalData.getBase(executionMode, credentials);
const nodeTypes = NodeTypes();
const additionalData = await WorkflowExecuteAdditionalData.get(executionMode, workflowData, workflowInstance, sessionId); const workflowInstance = new Workflow(workflowData.id, workflowData.nodes, workflowData.connections, false, nodeTypes, undefined, workflowData.settings);
const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, sessionId, destinationNode);
const workflowExecute = new WorkflowExecute(additionalData, executionMode); if (needsWebhook === true) {
return {
let executionId: string; waitingForWebhook: true,
};
if (runData === undefined || startNodes === undefined || startNodes.length === 0 || destinationNode === undefined) {
// Execute all nodes
if (WorkflowHelpers.isWorkflowIdValid(workflowData.id) === true) {
// Webhooks can only be tested with saved workflows
const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, sessionId, destinationNode);
if (needsWebhook === true) {
return {
waitingForWebhook: true,
};
}
} }
// Can execute without webhook so go on
executionId = await workflowExecute.run(workflowInstance, undefined, destinationNode);
} else {
// Execute only the nodes between start and destination nodes
executionId = await workflowExecute.runPartialWorkflow(workflowInstance, runData, startNodes, destinationNode);
} }
// For manual testing always set to not active
workflowData.active = false;
const credentials = await WorkflowCredentials(workflowData.nodes);
// Start the workflow
const data: IWorkflowExecutionDataProcess = {
credentials,
destinationNode,
executionMode,
runData,
sessionId,
startNodes,
workflowData,
};
const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(data);
return { return {
executionId, executionId,
}; };
@ -444,12 +446,11 @@ class App {
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
const executionMode = 'manual'; const executionMode = 'manual';
const sessionId = GenericHelpers.getSessionId(req);
const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, credentials); const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, credentials);
const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase; const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase;
const additionalData = await WorkflowExecuteAdditionalData.get(executionMode, workflowData, loadDataInstance.workflow, sessionId); const workflowCredentials = await WorkflowCredentials(workflowData.nodes);
const additionalData = await WorkflowExecuteAdditionalData.getBase(executionMode, workflowCredentials);
return loadDataInstance.getOptions(methodName, additionalData); return loadDataInstance.getOptions(methodName, additionalData);
})); }));
@ -843,13 +844,22 @@ class App {
const executionMode = 'retry'; const executionMode = 'retry';
const nodeTypes = NodeTypes(); const credentials = await WorkflowCredentials(fullExecutionData.workflowData.nodes);
const workflowInstance = new Workflow(req.params.id, fullExecutionData.workflowData.nodes, fullExecutionData.workflowData.connections, false, nodeTypes, fullExecutionData.workflowData.staticData, fullExecutionData.workflowData.settings);
const additionalData = await WorkflowExecuteAdditionalData.get(executionMode, fullExecutionData.workflowData, workflowInstance, undefined, req.params.id); fullExecutionData.workflowData.active = false;
const workflowExecute = new WorkflowExecute(additionalData, executionMode);
return workflowExecute.runExecutionData(workflowInstance, fullExecutionData.data); // Start the workflow
const data: IWorkflowExecutionDataProcess = {
credentials,
executionMode,
executionData: fullExecutionData.data,
retryOf: req.params.id,
workflowData: fullExecutionData.workflowData,
};
const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(data);
return executionId;
})); }));

View file

@ -1,19 +1,21 @@
import * as express from 'express'; import * as express from 'express';
import { import {
ActiveExecutions,
GenericHelpers, GenericHelpers,
IExecutionDb, IExecutionDb,
IResponseCallbackData, IResponseCallbackData,
IWorkflowDb, IWorkflowDb,
IWorkflowExecutionDataProcess,
ResponseHelper, ResponseHelper,
WorkflowRunner,
WorkflowCredentials,
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
} from './'; } from './';
import { import {
BINARY_ENCODING, BINARY_ENCODING,
ActiveExecutions,
NodeExecuteFunctions, NodeExecuteFunctions,
WorkflowExecute,
} from 'n8n-core'; } from 'n8n-core';
import { import {
@ -124,8 +126,8 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
} }
// Prepare everything that is needed to run the workflow // Prepare everything that is needed to run the workflow
const additionalData = await WorkflowExecuteAdditionalData.get(executionMode, workflowData, webhookData.workflow, sessionId); const credentials = await WorkflowCredentials(workflowData.nodes);
const workflowExecute = new WorkflowExecute(additionalData, executionMode); const additionalData = await WorkflowExecuteAdditionalData.getBase(executionMode, credentials);
// Add the Response and Request so that this data can be accessed in the node // Add the Response and Request so that this data can be accessed in the node
additionalData.httpRequest = req; additionalData.httpRequest = req;
@ -207,8 +209,17 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
}, },
}; };
const runData: IWorkflowExecutionDataProcess = {
credentials,
executionMode,
executionData: runExecutionData,
sessionId,
workflowData,
};
// Start now to run the workflow // Start now to run the workflow
const executionId = await workflowExecute.runExecutionData(webhookData.workflow, runExecutionData); const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(runData);
// Get a promise which resolves when the workflow did execute and send then response // Get a promise which resolves when the workflow did execute and send then response
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<IExecutionDb | undefined>; const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<IExecutionDb | undefined>;

View file

@ -3,14 +3,11 @@ import {
IExecutionDb, IExecutionDb,
IExecutionFlattedDb, IExecutionFlattedDb,
IPushDataExecutionFinished, IPushDataExecutionFinished,
IPushDataExecutionStarted,
IPushDataNodeExecuteAfter,
IPushDataNodeExecuteBefore,
IWorkflowBase, IWorkflowBase,
IWorkflowExecutionDataProcess,
Push, Push,
ResponseHelper, ResponseHelper,
WebhookHelpers, WebhookHelpers,
WorkflowCredentials,
WorkflowHelpers, WorkflowHelpers,
} from './'; } from './';
@ -19,11 +16,13 @@ import {
} from "n8n-core"; } from "n8n-core";
import { import {
IDataObject,
IRun, IRun,
ITaskData, ITaskData,
IWorkflowCredentials,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
IWorkflowExecuteHooks,
WorkflowExecuteMode, WorkflowExecuteMode,
Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
@ -68,7 +67,7 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
* @param {string} executionIdActive The id of the finished execution * @param {string} executionIdActive The id of the finished execution
* @param {string} [executionIdDb] The database id of finished execution * @param {string} [executionIdDb] The database id of finished execution
*/ */
function pushExecutionFinished(fullRunData: IRun, executionIdActive: string, executionIdDb?: string) { export function pushExecutionFinished(fullRunData: IRun, executionIdActive: string, executionIdDb?: string) {
// Clone the object except the runData. That one is not supposed // Clone the object except the runData. That one is not supposed
// to be send. Because that data got send piece by piece after // to be send. Because that data got send piece by piece after
// each node which finished executing // each node which finished executing
@ -94,70 +93,79 @@ function pushExecutionFinished(fullRunData: IRun, executionIdActive: string, exe
} }
const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowInstance: Workflow, sessionId?: string, retryOf?: string) => { /**
* Returns the workflow execution hooks
*
* @param {WorkflowExecuteMode} mode
* @param {IWorkflowBase} workflowData
* @param {string} executionId
* @param {string} [sessionId]
* @param {string} [retryOf]
* @returns {IWorkflowExecuteHooks}
*/
const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, executionId: string, sessionId?: string, retryOf?: string): IWorkflowExecuteHooks => {
return { return {
nodeExecuteBefore: [ nodeExecuteBefore: [
async (executionId: string, nodeName: string): Promise<void> => { async (nodeName: string): Promise<void> => {
// Push data to session which started workflow before each
// node which starts rendering
if (sessionId === undefined) { if (sessionId === undefined) {
// Only push data to the session which started it
return; return;
} }
const sendData: IPushDataNodeExecuteBefore = { pushInstance.send('nodeExecuteBefore', {
executionId, executionId,
nodeName, nodeName,
}; }, sessionId);
pushInstance.send('nodeExecuteBefore', sendData, sessionId);
}, },
], ],
nodeExecuteAfter: [ nodeExecuteAfter: [
async (executionId: string, nodeName: string, data: ITaskData): Promise<void> => { async (nodeName: string, data: ITaskData): Promise<void> => {
// Push data to session which started workflow after each rendered node
if (sessionId === undefined) { if (sessionId === undefined) {
return; return;
} }
const sendData: IPushDataNodeExecuteAfter = { pushInstance.send('nodeExecuteAfter', {
executionId, executionId,
nodeName, nodeName,
data, data,
}; }, sessionId);
pushInstance.send('nodeExecuteAfter', sendData, sessionId);
}, },
], ],
workflowExecuteBefore: [ workflowExecuteBefore: [
async (executionId: string): Promise<void> => { async (): Promise<void> => {
// Push data to editor-ui once workflow finished // Push data to editor-ui once workflow finished
const sendData: IPushDataExecutionStarted = { pushInstance.send('executionStarted', {
executionId, executionId,
mode, mode,
startedAt: new Date(), startedAt: new Date(),
retryOf, retryOf,
workflowId: workflowData.id as string, workflowId: workflowData.id as string,
workflowName: workflowData.name, workflowName: workflowData.name,
}; });
pushInstance.send('executionStarted', sendData);
} }
], ],
workflowExecuteAfter: [ workflowExecuteAfter: [
async (fullRunData: IRun, executionId: string): Promise<void> => { async (fullRunData: IRun, newStaticData: IDataObject): Promise<void> => {
try { try {
const workflowSavePromise = WorkflowHelpers.saveStaticData(workflowInstance); if (WorkflowHelpers.isWorkflowIdValid(workflowData.id as string) === true) {
// Workflow is saved so update in database
try {
await WorkflowHelpers.saveStaticDataById(workflowData.id as string, newStaticData);
} catch (e) {
// TODO: Add proper logging!
console.error(`There was a problem saving the workflow with id "${workflowData.id}" to save changed staticData: ${e.message}`);
}
}
let saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean; let saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
if (workflowInstance.settings !== undefined && workflowInstance.settings.saveManualExecutions !== undefined) { if (workflowData.settings !== undefined && workflowData.settings.saveManualExecutions !== undefined) {
// Apply to workflow override // Apply to workflow override
saveManualExecutions = workflowInstance.settings.saveManualExecutions as boolean; saveManualExecutions = workflowData.settings.saveManualExecutions as boolean;
} }
if (mode === 'manual' && saveManualExecutions === false) { if (mode === 'manual' && saveManualExecutions === false) {
if (workflowSavePromise !== undefined) {
// If workflow had to be saved wait till it is done
await workflowSavePromise;
}
pushExecutionFinished(fullRunData, executionId); pushExecutionFinished(fullRunData, executionId);
executeErrorWorkflow(workflowData, fullRunData, mode); executeErrorWorkflow(workflowData, fullRunData, mode);
return; return;
@ -166,9 +174,9 @@ const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowI
// Check config to know if execution should be saved or not // Check config to know if execution should be saved or not
let saveDataErrorExecution = config.get('executions.saveDataOnError') as string; let saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
if (workflowInstance.settings !== undefined) { if (workflowData.settings !== undefined) {
saveDataErrorExecution = (workflowInstance.settings.saveDataErrorExecution as string) || saveDataErrorExecution; saveDataErrorExecution = (workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution;
saveDataSuccessExecution = (workflowInstance.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution; saveDataSuccessExecution = (workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution;
} }
const workflowDidSucceed = !fullRunData.data.resultData.error; const workflowDidSucceed = !fullRunData.data.resultData.error;
@ -208,11 +216,6 @@ const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowI
await Db.collections.Execution!.update(retryOf, { retrySuccessId: executionResult.id }); await Db.collections.Execution!.update(retryOf, { retrySuccessId: executionResult.id });
} }
if (workflowSavePromise !== undefined) {
// If workflow had to be saved wait till it is done
await workflowSavePromise;
}
pushExecutionFinished(fullRunData, executionId, executionResult.id as string); pushExecutionFinished(fullRunData, executionId, executionResult.id as string);
executeErrorWorkflow(workflowData, fullRunData, mode, executionResult ? executionResult.id as string : undefined); executeErrorWorkflow(workflowData, fullRunData, mode, executionResult ? executionResult.id as string : undefined);
} catch (error) { } catch (error) {
@ -225,7 +228,15 @@ const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowI
}; };
export async function get(mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowInstance: Workflow, sessionId?: string, retryOf?: string): Promise<IWorkflowExecuteAdditionalData> { /**
* Returns the base additional data without webhooks
*
* @export
* @param {WorkflowExecuteMode} mode
* @param {IWorkflowCredentials} credentials
* @returns {Promise<IWorkflowExecuteAdditionalData>}
*/
export async function getBase(mode: WorkflowExecuteMode, credentials: IWorkflowCredentials): Promise<IWorkflowExecuteAdditionalData> {
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
const timezone = config.get('generic.timezone') as string; const timezone = config.get('generic.timezone') as string;
@ -238,11 +249,23 @@ export async function get(mode: WorkflowExecuteMode, workflowData: IWorkflowBase
} }
return { return {
credentials: await WorkflowCredentials(workflowData.nodes), credentials,
hooks: hooks(mode, workflowData, workflowInstance, sessionId, retryOf),
encryptionKey, encryptionKey,
timezone, timezone,
webhookBaseUrl, webhookBaseUrl,
webhookTestBaseUrl, webhookTestBaseUrl,
}; };
} }
/**
* Returns the workflow hooks
*
* @export
* @param {IWorkflowExecutionDataProcess} data
* @param {string} executionId
* @returns {IWorkflowExecuteHooks}
*/
export function getHookMethods(data: IWorkflowExecutionDataProcess, executionId: string): IWorkflowExecuteHooks {
return hooks(data.executionMode, data.workflowData, executionId, data.sessionId, data.retryOf as string | undefined);
}

View file

@ -1,15 +1,14 @@
import { import {
Db, Db,
IWorkflowExecutionDataProcess,
IWorkflowErrorData, IWorkflowErrorData,
NodeTypes, NodeTypes,
WorkflowExecuteAdditionalData, WorkflowCredentials,
WorkflowRunner,
} from './'; } from './';
import { import {
WorkflowExecute, IDataObject,
} from 'n8n-core';
import {
IExecuteData, IExecuteData,
INode, INode,
IRunExecutionData, IRunExecutionData,
@ -80,10 +79,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
return; return;
} }
const additionalData = await WorkflowExecuteAdditionalData.get(executionMode, workflowData, workflowInstance);
// Can execute without webhook so go on // Can execute without webhook so go on
const workflowExecute = new WorkflowExecute(additionalData, executionMode);
// Initialize the data of the webhook node // Initialize the data of the webhook node
const nodeExecutionStack: IExecuteData[] = []; const nodeExecutionStack: IExecuteData[] = [];
@ -115,9 +111,17 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
}, },
}; };
// Start now to run the workflow const credentials = await WorkflowCredentials(workflowData.nodes);
await workflowExecute.runExecutionData(workflowInstance, runExecutionData);
const runData: IWorkflowExecutionDataProcess = {
credentials,
executionMode,
executionData: runExecutionData,
workflowData,
};
const workflowRunner = new WorkflowRunner();
await workflowRunner.run(runData);
} catch (error) { } catch (error) {
console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}": ${error.message}`); console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}": ${error.message}`);
} }
@ -138,10 +142,7 @@ export async function saveStaticData(workflow: Workflow): Promise <void> {
if (isWorkflowIdValid(workflow.id) === true) { if (isWorkflowIdValid(workflow.id) === true) {
// Workflow is saved so update in database // Workflow is saved so update in database
try { try {
await Db.collections.Workflow! await saveStaticDataById(workflow.id!, workflow.staticData);
.update(workflow.id!, {
staticData: workflow.staticData,
});
workflow.staticData.__dataChanged = false; workflow.staticData.__dataChanged = false;
} catch (e) { } catch (e) {
// TODO: Add proper logging! // TODO: Add proper logging!
@ -150,3 +151,20 @@ export async function saveStaticData(workflow: Workflow): Promise <void> {
} }
} }
} }
/**
* Saves the given static data on workflow
*
* @export
* @param {(string | number)} workflowId The id of the workflow to save data on
* @param {IDataObject} newStaticData The static data to save
* @returns {Promise<void>}
*/
export async function saveStaticDataById(workflowId: string | number, newStaticData: IDataObject): Promise<void> {
await Db.collections.Workflow!
.update(workflowId, {
staticData: newStaticData,
});
}

View file

@ -0,0 +1,191 @@
import {
ActiveExecutions,
IProcessMessageDataHook,
ITransferNodeTypes,
IWorkflowExecutionDataProcess,
IWorkflowExecutionDataProcessWithExecution,
NodeTypes,
Push,
WorkflowExecuteAdditionalData,
} from './';
import {
IProcessMessage,
} from 'n8n-core';
import {
IExecutionError,
INode,
IRun,
IWorkflowExecuteHooks,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { fork } from 'child_process';
export class WorkflowRunner {
activeExecutions: ActiveExecutions.ActiveExecutions;
push: Push.Push;
constructor() {
this.push = Push.getInstance();
this.activeExecutions = ActiveExecutions.getInstance();
}
/**
* Returns the data of the node types that are needed
* to execute the given nodes
*
* @param {INode[]} nodes
* @returns {ITransferNodeTypes}
* @memberof WorkflowRunner
*/
getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
const nodeTypes = NodeTypes();
// Check which node-types have to be loaded
const neededNodeTypes: string[] = [];
for (const node of nodes) {
if (!neededNodeTypes.includes(node.type)) {
neededNodeTypes.push(node.type);
}
}
// Get all the data of the needed node types that they
// can be loaded again in the process
const returnData: ITransferNodeTypes = {};
for (const nodeTypeName of neededNodeTypes) {
if (nodeTypes.nodeTypes[nodeTypeName] === undefined) {
throw new Error(`The NodeType "${nodeTypeName}" could not be found!`);
}
returnData[nodeTypeName] = {
className: nodeTypes.nodeTypes[nodeTypeName].type.constructor.name,
sourcePath: nodeTypes.nodeTypes[nodeTypeName].sourcePath,
};
}
return returnData;
}
/**
* The process did send a hook message so execute the appropiate hook
*
* @param {IWorkflowExecuteHooks} hookFunctions
* @param {IProcessMessageDataHook} hookData
* @memberof WorkflowRunner
*/
processHookMessage(hookFunctions: IWorkflowExecuteHooks, hookData: IProcessMessageDataHook) {
if (hookFunctions[hookData.hook] !== undefined && Array.isArray(hookFunctions[hookData.hook])) {
for (const hookFunction of hookFunctions[hookData.hook]!) {
// TODO: Not sure if that is 100% correct or something is still missing like to wait
hookFunction.apply(this, hookData.parameters)
.catch((error: Error) => {
// Catch all errors here because when "executeHook" gets called
// we have the most time no "await" and so the errors would so
// not be uncaught by anything.
// TODO: Add proper logging
console.error(`There was a problem executing hook: "${hookData.hook}"`);
console.error('Parameters:');
console.error(hookData.parameters);
console.error('Error:');
console.error(error);
});
}
}
}
/**
* The process did error
*
* @param {IExecutionError} error
* @param {Date} startedAt
* @param {WorkflowExecuteMode} executionMode
* @param {string} executionId
* @memberof WorkflowRunner
*/
processError(error: IExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string) {
const fullRunData: IRun = {
data: {
resultData: {
error,
runData: {},
},
},
finished: false,
mode: executionMode,
startedAt,
stoppedAt: new Date(),
};
// Remove from active execution with empty data. That will
// set the execution to failed.
this.activeExecutions.remove(executionId, fullRunData);
// Also send to Editor UI
WorkflowExecuteAdditionalData.pushExecutionFinished(fullRunData, executionId);
}
/**
* Run the workflow in subprocess
*
* @param {IWorkflowExecutionDataProcess} data
* @returns {Promise<string>}
* @memberof WorkflowRunner
*/
async run(data: IWorkflowExecutionDataProcess): Promise<string> {
const startedAt = new Date();
const subprocess = fork('./dist/src/WorkflowRunnerProcess.js');
// Register the active execution
const executionId = this.activeExecutions.add(subprocess, data);
const nodeTypeData = this.getNodeTypeData(data.workflowData.nodes);
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
const hookFunctions = WorkflowExecuteAdditionalData.getHookMethods(data, executionId);
// Send all data to subprocess it needs to run the workflow
subprocess.send({ type: 'startWorkflow', data } as IProcessMessage);
// Listen to data from the subprocess
subprocess.on('message', (message: IProcessMessage) => {
if (message.type === 'end') {
this.activeExecutions.remove(executionId!, message.data.runData);
} else if (message.type === 'processError') {
const executionError = message.data.executionError as IExecutionError;
this.processError(executionError, startedAt, data.executionMode, executionId);
} else if (message.type === 'processHook') {
this.processHookMessage(hookFunctions, message.data as IProcessMessageDataHook);
}
});
// Also get informed when the processes does exit especially when it did crash
subprocess.on('exit', (code, signal) => {
if (code !== 0) {
// Process did exit with error code, so something went wrong.
const executionError = {
message: 'Workflow execution process did crash for an unknown reason!',
} as IExecutionError;
this.processError(executionError, startedAt, data.executionMode, executionId);
}
});
return executionId;
}
}

View file

@ -0,0 +1,208 @@
import {
IProcessMessageDataHook,
IWorkflowExecutionDataProcessWithExecution,
NodeTypes,
WorkflowExecuteAdditionalData,
} from './';
import {
IProcessMessage,
WorkflowExecute,
} from 'n8n-core';
import {
IDataObject,
IExecutionError,
INodeType,
INodeTypeData,
IRun,
ITaskData,
IWorkflowExecuteHooks,
Workflow,
} from 'n8n-workflow';
import { ChildProcess } from 'child_process';
export class WorkflowRunnerProcess {
data: IWorkflowExecutionDataProcessWithExecution | undefined;
startedAt = new Date();
workflow: Workflow | undefined;
workflowExecute: WorkflowExecute | undefined;
async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> {
this.data = inputData;
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);
this.workflow = new Workflow(this.data.workflowData.id as string | undefined, this.data.workflowData!.nodes, this.data.workflowData!.connections, this.data.workflowData!.active, nodeTypes, this.data.workflowData!.staticData);
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.executionMode, this.data.credentials);
additionalData.hooks = this.getProcessForwardHooks();
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
// 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);
}
}
sendHookToParentProcess(hook: string, parameters: any[]) { // tslint:disable-line:no-any
(process as unknown as ChildProcess).send({
type: 'processHook',
data: {
hook,
parameters,
} as IProcessMessageDataHook,
} as IProcessMessage);
}
/**
* 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
*
* @param {ChildProcess} process
* @returns
*/
getProcessForwardHooks(): IWorkflowExecuteHooks {
return {
nodeExecuteBefore: [
async (nodeName: string): Promise<void> => {
this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]);
},
],
nodeExecuteAfter: [
async (nodeName: string, data: ITaskData): Promise<void> => {
this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data]);
},
],
workflowExecuteBefore: [
async (): Promise<void> => {
this.sendHookToParentProcess('workflowExecuteBefore', []);
}
],
workflowExecuteAfter: [
async (fullRunData: IRun, newStaticData?: IDataObject): Promise<void> => {
this.sendHookToParentProcess('workflowExecuteAfter', [fullRunData, newStaticData]);
},
]
};
}
}
/**
* Sends data to parent process
*
* @param {string} type The type of data to send
* @param {*} data The data
*/
function sendToParentProcess(type: string, data: any): void { // tslint:disable-line:no-any
process.send!({
type,
data,
});
}
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') {
const runData = await workflowRunner.runWorkflow(message.data);
sendToParentProcess('end', {
runData,
});
// Once the workflow got executed make sure the process gets killed again
process.exit();
} else if (message.type === 'stopExecution') {
// The workflow execution should be stopped
let fullRunData: IRun;
if (workflowRunner.workflowExecute !== undefined) {
// Workflow started already executing
fullRunData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);
// If there is any data send it to parent process
await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!);
} else {
// Workflow did not get started yet
fullRunData = {
data: {
resultData: {
runData: {},
},
},
finished: true,
mode: workflowRunner.data!.executionMode,
startedAt: workflowRunner.startedAt,
stoppedAt: new Date(),
};
workflowRunner.sendHookToParentProcess('workflowExecuteAfter', [fullRunData]);
}
sendToParentProcess('end', {
fullRunData,
});
// Stop process
process.exit();
}
} catch (error) {
// Catch all uncaught errors and forward them to parent process
const executionError = {
message: error.message,
stack: error.stack,
} as IExecutionError;
sendToParentProcess('processError', {
executionError,
});
process.exit();
}
});

View file

@ -3,8 +3,9 @@ export * from './Interfaces';
export * from './LoadNodesAndCredentials'; export * from './LoadNodesAndCredentials';
export * from './NodeTypes'; export * from './NodeTypes';
export * from './WorkflowCredentials'; export * from './WorkflowCredentials';
export * from './WorkflowRunner';
import * as ActiveExecutions from './ActiveExecutions';
import * as ActiveWorkflowRunner from './ActiveWorkflowRunner'; import * as ActiveWorkflowRunner from './ActiveWorkflowRunner';
import * as Db from './Db'; import * as Db from './Db';
import * as GenericHelpers from './GenericHelpers'; import * as GenericHelpers from './GenericHelpers';
@ -16,6 +17,7 @@ import * as WebhookHelpers from './WebhookHelpers';
import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData';
import * as WorkflowHelpers from './WorkflowHelpers'; import * as WorkflowHelpers from './WorkflowHelpers';
export { export {
ActiveExecutions,
ActiveWorkflowRunner, ActiveWorkflowRunner,
Db, Db,
GenericHelpers, GenericHelpers,

View file

@ -1,13 +1,10 @@
import { import {
IGetExecuteTriggerFunctions,
ITriggerResponse, ITriggerResponse,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
Workflow, Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import {
NodeExecuteFunctions,
} from './';
export interface WorkflowData { export interface WorkflowData {
workflow: Workflow; workflow: Workflow;
@ -65,7 +62,7 @@ export class ActiveWorkflows {
* @returns {Promise<void>} * @returns {Promise<void>}
* @memberof ActiveWorkflows * @memberof ActiveWorkflows
*/ */
async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData): Promise<void> { async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions): Promise<void> {
console.log('ADD ID (active): ' + id); console.log('ADD ID (active): ' + id);
this.workflowData[id] = { this.workflowData[id] = {
@ -75,7 +72,7 @@ export class ActiveWorkflows {
let triggerResponse: ITriggerResponse | undefined; let triggerResponse: ITriggerResponse | undefined;
for (const triggerNode of triggerNodes) { for (const triggerNode of triggerNodes) {
triggerResponse = await workflow.runTrigger(triggerNode, NodeExecuteFunctions, additionalData, 'trigger'); triggerResponse = await workflow.runTrigger(triggerNode, getTriggerFunctions, additionalData, 'trigger');
if (triggerResponse !== undefined) { if (triggerResponse !== undefined) {
// If a response was given save it // If a response was given save it
this.workflowData[id].triggerResponse = triggerResponse; this.workflowData[id].triggerResponse = triggerResponse;

View file

@ -8,18 +8,12 @@ import {
ILoadOptionsFunctions as ILoadOptionsFunctionsBase, ILoadOptionsFunctions as ILoadOptionsFunctionsBase,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
IRun,
IRunExecutionData,
ITriggerFunctions as ITriggerFunctionsBase, ITriggerFunctions as ITriggerFunctionsBase,
IWebhookFunctions as IWebhookFunctionsBase, IWebhookFunctions as IWebhookFunctionsBase,
IWorkflowSettings as IWorkflowSettingsWorkflow, IWorkflowSettings as IWorkflowSettingsWorkflow,
Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import {
IDeferredPromise
} from '.';
import * as request from 'request'; import * as request from 'request';
import * as requestPromise from 'request-promise-native'; import * as requestPromise from 'request-promise-native';
@ -29,6 +23,12 @@ interface Constructable<T> {
} }
export interface IProcessMessage {
data?: any; // tslint:disable-line:no-any
type: string;
}
export interface IExecuteFunctions extends IExecuteFunctionsBase { export interface IExecuteFunctions extends IExecuteFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
@ -45,13 +45,6 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
}; };
} }
export interface IExecutingWorkflowData {
runExecutionData: IRunExecutionData;
startedAt: Date;
mode: WorkflowExecuteMode;
workflow: Workflow;
postExecutePromises: Array<IDeferredPromise<IRun>>;
}
export interface IExecutionsCurrentSummary { export interface IExecutionsCurrentSummary {
id: string; id: string;

View file

@ -314,33 +314,12 @@ export function getWebhookDescription(name: string, workflow: Workflow, node: IN
* @param {WorkflowExecuteMode} mode * @param {WorkflowExecuteMode} mode
* @returns {ITriggerFunctions} * @returns {ITriggerFunctions}
*/ */
// TODO: Check if I can get rid of: additionalData, and so then maybe also at ActiveWorkflowRunner.add
export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions { export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions {
return ((workflow: Workflow, node: INode) => { return ((workflow: Workflow, node: INode) => {
return { return {
emit: (data: INodeExecutionData[][]): void => { emit: (data: INodeExecutionData[][]): void => {
const workflowExecute = new WorkflowExecute(additionalData, mode); throw new Error('Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function!');
const nodeExecutionStack: IExecuteData[] = [
{
node,
data: {
main: data,
}
}
];
const runExecutionData: IRunExecutionData = {
startData: {},
resultData: {
runData: {},
},
executionData: {
contextData: {},
nodeExecutionStack,
waitingExecution: {},
},
};
workflowExecute.runExecutionData(workflow, runExecutionData);
}, },
getCredentials(type: string): ICredentialDataDecryptedObject | undefined { getCredentials(type: string): ICredentialDataDecryptedObject | undefined {
return getCredentials(workflow, node, type, additionalData); return getCredentials(workflow, node, type, additionalData);

View file

@ -1,5 +1,6 @@
import { import {
IConnection, IConnection,
IDataObject,
IExecuteData, IExecuteData,
IExecutionError, IExecutionError,
INode, INode,
@ -16,21 +17,30 @@ import {
Workflow, Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
ActiveExecutions,
NodeExecuteFunctions, NodeExecuteFunctions,
} from './'; } from './';
export class WorkflowExecute { export class WorkflowExecute {
runExecutionData: IRunExecutionData;
private additionalData: IWorkflowExecuteAdditionalData; private additionalData: IWorkflowExecuteAdditionalData;
private mode: WorkflowExecuteMode; private mode: WorkflowExecuteMode;
private activeExecutions: ActiveExecutions.ActiveExecutions;
private executionId: string | null = null;
constructor(additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode) { constructor(additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, runExecutionData?: IRunExecutionData) {
this.additionalData = additionalData; this.additionalData = additionalData;
this.activeExecutions = ActiveExecutions.getInstance();
this.mode = mode; this.mode = mode;
this.runExecutionData = runExecutionData || {
startData: {
},
resultData: {
runData: {},
},
executionData: {
contextData: {},
nodeExecutionStack: [],
waitingExecution: {},
},
};
} }
@ -44,7 +54,7 @@ export class WorkflowExecute {
* @returns {(Promise<string>)} * @returns {(Promise<string>)}
* @memberof WorkflowExecute * @memberof WorkflowExecute
*/ */
async run(workflow: Workflow, startNode?: INode, destinationNode?: string): Promise<string> { async run(workflow: Workflow, startNode?: INode, destinationNode?: string): Promise<IRun> {
// Get the nodes to start workflow execution from // Get the nodes to start workflow execution from
startNode = startNode || workflow.getStartNode(destinationNode); startNode = startNode || workflow.getStartNode(destinationNode);
@ -75,7 +85,7 @@ export class WorkflowExecute {
} }
]; ];
const runExecutionData: IRunExecutionData = { this.runExecutionData = {
startData: { startData: {
destinationNode, destinationNode,
runNodeFilter, runNodeFilter,
@ -90,7 +100,7 @@ export class WorkflowExecute {
}, },
}; };
return this.runExecutionData(workflow, runExecutionData); return this.processRunExecutionData(workflow);
} }
@ -105,8 +115,7 @@ export class WorkflowExecute {
* @returns {(Promise<string>)} * @returns {(Promise<string>)}
* @memberof WorkflowExecute * @memberof WorkflowExecute
*/ */
async runPartialWorkflow(workflow: Workflow, runData: IRunData, startNodes: string[], destinationNode: string): Promise<string> { async runPartialWorkflow(workflow: Workflow, runData: IRunData, startNodes: string[], destinationNode: string): Promise<IRun> {
let incomingNodeConnections: INodeConnections | undefined; let incomingNodeConnections: INodeConnections | undefined;
let connection: IConnection; let connection: IConnection;
@ -185,8 +194,7 @@ export class WorkflowExecute {
runNodeFilter = workflow.getParentNodes(destinationNode); runNodeFilter = workflow.getParentNodes(destinationNode);
runNodeFilter.push(destinationNode); runNodeFilter.push(destinationNode);
this.runExecutionData = {
const runExecutionData: IRunExecutionData = {
startData: { startData: {
destinationNode, destinationNode,
runNodeFilter, runNodeFilter,
@ -201,7 +209,7 @@ export class WorkflowExecute {
}, },
}; };
return await this.runExecutionData(workflow, runExecutionData); return await this.processRunExecutionData(workflow);
} }
@ -240,7 +248,7 @@ export class WorkflowExecute {
} }
addNodeToBeExecuted(workflow: Workflow, runExecutionData: IRunExecutionData, connectionData: IConnection, outputIndex: number, parentNodeName: string, nodeSuccessData: INodeExecutionData[][], runIndex: number): void { addNodeToBeExecuted(workflow: Workflow, connectionData: IConnection, outputIndex: number, parentNodeName: string, nodeSuccessData: INodeExecutionData[][], runIndex: number): void {
let stillDataMissing = false; let stillDataMissing = false;
// Check if node has multiple inputs as then we have to wait for all input data // Check if node has multiple inputs as then we have to wait for all input data
@ -250,33 +258,33 @@ export class WorkflowExecute {
let nodeWasWaiting = true; let nodeWasWaiting = true;
// Check if there is already data for the node // Check if there is already data for the node
if (runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined) { if (this.runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined) {
// Node does not have data yet so create a new empty one // Node does not have data yet so create a new empty one
runExecutionData.executionData!.waitingExecution[connectionData.node] = {}; this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
nodeWasWaiting = false; nodeWasWaiting = false;
} }
if (runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] === undefined) { if (this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] === undefined) {
// Node does not have data for runIndex yet so create also empty one and init it // Node does not have data for runIndex yet so create also empty one and init it
runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: [] main: []
}; };
for (let i = 0; i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; i++) { for (let i = 0; i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; i++) {
runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.push(null); this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.push(null);
} }
} }
// Add the new data // Add the new data
if (nodeSuccessData === null) { if (nodeSuccessData === null) {
runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[connectionData.index] = null; this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[connectionData.index] = null;
} else { } else {
runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[connectionData.index] = nodeSuccessData[outputIndex]; this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[connectionData.index] = nodeSuccessData[outputIndex];
} }
// Check if all data exists now // Check if all data exists now
let thisExecutionData: INodeExecutionData[] | null; let thisExecutionData: INodeExecutionData[] | null;
let allDataFound = true; let allDataFound = true;
for (let i = 0; i < runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.length; i++) { for (let i = 0; i < this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.length; i++) {
thisExecutionData = runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[i]; thisExecutionData = this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[i];
if (thisExecutionData === null) { if (thisExecutionData === null) {
allDataFound = false; allDataFound = false;
break; break;
@ -286,17 +294,17 @@ export class WorkflowExecute {
if (allDataFound === true) { if (allDataFound === true) {
// All data exists for node to be executed // All data exists for node to be executed
// So add it to the execution stack // So add it to the execution stack
runExecutionData.executionData!.nodeExecutionStack.push({ this.runExecutionData.executionData!.nodeExecutionStack.push({
node: workflow.nodes[connectionData.node], node: workflow.nodes[connectionData.node],
data: runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]
}); });
// Remove the data from waiting // Remove the data from waiting
delete runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]; delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex];
if (Object.keys(runExecutionData.executionData!.waitingExecution[connectionData.node]).length === 0) { if (Object.keys(this.runExecutionData.executionData!.waitingExecution[connectionData.node]).length === 0) {
// No more data left for the node so also delete that one // No more data left for the node so also delete that one
delete runExecutionData.executionData!.waitingExecution[connectionData.node]; delete this.runExecutionData.executionData!.waitingExecution[connectionData.node];
} }
return; return;
} else { } else {
@ -327,7 +335,7 @@ export class WorkflowExecute {
continue; continue;
} }
const executionStackNodes = runExecutionData.executionData!.nodeExecutionStack.map((stackData) => stackData.node.name); const executionStackNodes = this.runExecutionData.executionData!.nodeExecutionStack.map((stackData) => stackData.node.name);
// Check if that node is also an output connection of the // Check if that node is also an output connection of the
// previously processed one // previously processed one
@ -345,7 +353,7 @@ export class WorkflowExecute {
} }
// Check if node got processed already // Check if node got processed already
if (runExecutionData.resultData.runData[inputData.node] !== undefined) { if (this.runExecutionData.resultData.runData[inputData.node] !== undefined) {
// Node got processed already so no need to add it // Node got processed already so no need to add it
continue; continue;
} }
@ -376,7 +384,7 @@ export class WorkflowExecute {
} }
// Check if node got processed already // Check if node got processed already
if (runExecutionData.resultData.runData[parentNode] !== undefined) { if (this.runExecutionData.resultData.runData[parentNode] !== undefined) {
// Node got processed already so we can use the // Node got processed already so we can use the
// output data as input of this node // output data as input of this node
break; break;
@ -393,7 +401,7 @@ export class WorkflowExecute {
if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) { if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) {
// Add only node if it does not have any inputs becuase else it will // Add only node if it does not have any inputs becuase else it will
// be added by its input node later anyway. // be added by its input node later anyway.
runExecutionData.executionData!.nodeExecutionStack.push( this.runExecutionData.executionData!.nodeExecutionStack.push(
{ {
node: workflow.getNode(nodeToAdd) as INode, node: workflow.getNode(nodeToAdd) as INode,
data: { data: {
@ -428,15 +436,15 @@ export class WorkflowExecute {
if (stillDataMissing === true) { if (stillDataMissing === true) {
// Additional data is needed to run node so add it to waiting // Additional data is needed to run node so add it to waiting
if (!runExecutionData.executionData!.waitingExecution.hasOwnProperty(connectionData.node)) { if (!this.runExecutionData.executionData!.waitingExecution.hasOwnProperty(connectionData.node)) {
runExecutionData.executionData!.waitingExecution[connectionData.node] = {}; this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
} }
runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: connectionDataArray main: connectionDataArray
}; };
} else { } else {
// All data is there so add it directly to stack // All data is there so add it directly to stack
runExecutionData.executionData!.nodeExecutionStack.push({ this.runExecutionData.executionData!.nodeExecutionStack.push({
node: workflow.nodes[connectionData.node], node: workflow.nodes[connectionData.node],
data: { data: {
main: connectionDataArray main: connectionDataArray
@ -450,12 +458,11 @@ export class WorkflowExecute {
* Runs the given execution data. * Runs the given execution data.
* *
* @param {Workflow} workflow * @param {Workflow} workflow
* @param {IRunExecutionData} runExecutionData
* @returns {Promise<string>} * @returns {Promise<string>}
* @memberof WorkflowExecute * @memberof WorkflowExecute
*/ */
async runExecutionData(workflow: Workflow, runExecutionData: IRunExecutionData): Promise<string> { async processRunExecutionData(workflow: Workflow): Promise<IRun> {
const startedAt = new Date().getTime(); const startedAt = new Date();
const workflowIssues = workflow.checkReadyForExecution(); const workflowIssues = workflow.checkReadyForExecution();
if (workflowIssues !== null) { if (workflowIssues !== null) {
@ -471,39 +478,29 @@ export class WorkflowExecute {
let startTime: number; let startTime: number;
let taskData: ITaskData; let taskData: ITaskData;
if (runExecutionData.startData === undefined) { if (this.runExecutionData.startData === undefined) {
runExecutionData.startData = {}; this.runExecutionData.startData = {};
} }
this.executionId = this.activeExecutions.add(workflow, runExecutionData, this.mode); this.executeHook('workflowExecuteBefore', []);
this.executeHook('workflowExecuteBefore', [this.executionId]);
let currentExecutionTry = ''; let currentExecutionTry = '';
let lastExecutionTry = ''; let lastExecutionTry = '';
// Wait for the next tick so that the executionId gets already returned. return (async () => {
// So it can directly be send to the editor-ui and is so aware of the
// executionId when the first push messages arrive.
process.nextTick(() => (async () => {
executionLoop: executionLoop:
while (runExecutionData.executionData!.nodeExecutionStack.length !== 0) { while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) {
if (this.activeExecutions.shouldBeStopped(this.executionId!) === true) {
// The execution should be stopped
break;
}
nodeSuccessData = null; nodeSuccessData = null;
executionError = undefined; executionError = undefined;
executionData = runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData; executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
executionNode = executionData.node; executionNode = executionData.node;
this.executeHook('nodeExecuteBefore', [this.executionId, executionNode.name]); this.executeHook('nodeExecuteBefore', [executionNode.name]);
// Get the index of the current run // Get the index of the current run
runIndex = 0; runIndex = 0;
if (runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) { if (this.runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) {
runIndex = runExecutionData.resultData.runData[executionNode.name].length; runIndex = this.runExecutionData.resultData.runData[executionNode.name].length;
} }
currentExecutionTry = `${executionNode.name}:${runIndex}`; currentExecutionTry = `${executionNode.name}:${runIndex}`;
@ -512,7 +509,7 @@ export class WorkflowExecute {
throw new Error('Did stop execution because execution seems to be in endless loop.'); throw new Error('Did stop execution because execution seems to be in endless loop.');
} }
if (runExecutionData.startData!.runNodeFilter !== undefined && runExecutionData.startData!.runNodeFilter!.indexOf(executionNode.name) === -1) { if (this.runExecutionData.startData!.runNodeFilter !== undefined && this.runExecutionData.startData!.runNodeFilter!.indexOf(executionNode.name) === -1) {
// If filter is set and node is not on filter skip it, that avoids the problem that it executes // If filter is set and node is not on filter skip it, that avoids the problem that it executes
// leafs that are parallel to a selected destinationNode. Normally it would execute them because // leafs that are parallel to a selected destinationNode. Normally it would execute them because
// they have the same parent and it executes all child nodes. // they have the same parent and it executes all child nodes.
@ -539,7 +536,7 @@ export class WorkflowExecute {
if (!executionData.data!.hasOwnProperty('main')) { if (!executionData.data!.hasOwnProperty('main')) {
// ExecutionData does not even have the connection set up so can // ExecutionData does not even have the connection set up so can
// not have that data, so add it again to be executed later // not have that data, so add it again to be executed later
runExecutionData.executionData!.nodeExecutionStack.push(executionData); this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
lastExecutionTry = currentExecutionTry; lastExecutionTry = currentExecutionTry;
continue executionLoop; continue executionLoop;
} }
@ -549,7 +546,7 @@ export class WorkflowExecute {
// of both inputs has to be available to be able to process the node. // of both inputs has to be available to be able to process the node.
if (executionData.data!.main!.length < connectionIndex || executionData.data!.main![connectionIndex] === null) { if (executionData.data!.main!.length < connectionIndex || executionData.data!.main![connectionIndex] === null) {
// Does not have the data of the connections so add back to stack // Does not have the data of the connections so add back to stack
runExecutionData.executionData!.nodeExecutionStack.push(executionData); this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
lastExecutionTry = currentExecutionTry; lastExecutionTry = currentExecutionTry;
continue executionLoop; continue executionLoop;
} }
@ -591,15 +588,8 @@ export class WorkflowExecute {
} }
} }
// Check again if the execution should be stopped else it this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
// could take forever to stop when each try takes a long time nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
if (this.activeExecutions.shouldBeStopped(this.executionId!) === true) {
// The execution should be stopped
break;
}
runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
if (nodeSuccessData === null) { if (nodeSuccessData === null) {
// If null gets returned it means that the node did succeed // If null gets returned it means that the node did succeed
@ -620,8 +610,8 @@ export class WorkflowExecute {
// Add the data to return to the user // Add the data to return to the user
// (currently does not get cloned as data does not get changed, maybe later we should do that?!?!) // (currently does not get cloned as data does not get changed, maybe later we should do that?!?!)
if (!runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) { if (!this.runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) {
runExecutionData.resultData.runData[executionNode.name] = []; this.runExecutionData.resultData.runData[executionNode.name] = [];
} }
taskData = { taskData = {
startTime, startTime,
@ -642,12 +632,12 @@ export class WorkflowExecute {
} }
} else { } else {
// Node execution did fail so add error and stop execution // Node execution did fail so add error and stop execution
runExecutionData.resultData.runData[executionNode.name].push(taskData); this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
// Add the execution data again so that it can get restarted // Add the execution data again so that it can get restarted
runExecutionData.executionData!.nodeExecutionStack.unshift(executionData); this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
this.executeHook('nodeExecuteAfter', [this.executionId, executionNode.name, taskData]); this.executeHook('nodeExecuteAfter', [executionNode.name, taskData]);
break; break;
} }
@ -658,11 +648,11 @@ export class WorkflowExecute {
'main': nodeSuccessData 'main': nodeSuccessData
} as ITaskDataConnections); } as ITaskDataConnections);
this.executeHook('nodeExecuteAfter', [this.executionId, executionNode.name, taskData]); this.executeHook('nodeExecuteAfter', [executionNode.name, taskData]);
runExecutionData.resultData.runData[executionNode.name].push(taskData); this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
if (runExecutionData.startData && runExecutionData.startData.destinationNode && runExecutionData.startData.destinationNode === executionNode.name) { if (this.runExecutionData.startData && this.runExecutionData.startData.destinationNode && this.runExecutionData.startData.destinationNode === executionNode.name) {
// If destination node is defined and got executed stop execution // If destination node is defined and got executed stop execution
continue; continue;
} }
@ -686,7 +676,7 @@ export class WorkflowExecute {
return Promise.reject(new Error(`The node "${executionNode.name}" connects to not found node "${connectionData.node}"`)); return Promise.reject(new Error(`The node "${executionNode.name}" connects to not found node "${connectionData.node}"`));
} }
this.addNodeToBeExecuted(workflow, runExecutionData, connectionData, parseInt(outputIndex, 10), executionNode.name, nodeSuccessData!, runIndex); this.addNodeToBeExecuted(workflow, connectionData, parseInt(outputIndex, 10), executionNode.name, nodeSuccessData!, runIndex);
} }
} }
} }
@ -696,45 +686,61 @@ export class WorkflowExecute {
return Promise.resolve(); return Promise.resolve();
})() })()
.then(async () => { .then(async () => {
const fullRunData: IRun = { return this.processSuccessExecution(startedAt, workflow, executionError);
data: runExecutionData,
mode: this.mode,
startedAt: new Date(startedAt),
stoppedAt: new Date(),
};
if (executionError !== undefined) {
fullRunData.data.resultData.error = executionError;
} else {
fullRunData.finished = true;
}
this.activeExecutions.remove(this.executionId!, fullRunData);
await this.executeHook('workflowExecuteAfter', [fullRunData, this.executionId!]);
return fullRunData;
}) })
.catch(async (error) => { .catch(async (error) => {
const fullRunData: IRun = { const fullRunData = this.getFullRunData(startedAt);
data: runExecutionData,
mode: this.mode,
startedAt: new Date(startedAt),
stoppedAt: new Date(),
};
fullRunData.data.resultData.error = { fullRunData.data.resultData.error = {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
}; };
this.activeExecutions.remove(this.executionId!, fullRunData); // Check if static data changed
let newStaticData: IDataObject | undefined;
if (workflow.staticData.__dataChanged === true) {
// Static data of workflow changed
newStaticData = workflow.staticData;
}
await this.executeHook('workflowExecuteAfter', [fullRunData, this.executionId!]); await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]);
return fullRunData; return fullRunData;
})); });
return this.executionId;
} }
async processSuccessExecution(startedAt: Date, workflow: Workflow, executionError?: IExecutionError): Promise<IRun> {
const fullRunData = this.getFullRunData(startedAt);
if (executionError !== undefined) {
fullRunData.data.resultData.error = executionError;
} else {
fullRunData.finished = true;
}
// Check if static data changed
let newStaticData: IDataObject | undefined;
if (workflow.staticData.__dataChanged === true) {
// Static data of workflow changed
newStaticData = workflow.staticData;
}
await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]);
return fullRunData;
}
getFullRunData(startedAt: Date): IRun {
const fullRunData: IRun = {
data: this.runExecutionData,
mode: this.mode,
startedAt,
stoppedAt: new Date(),
};
return fullRunData;
}
} }

View file

@ -14,11 +14,9 @@ export * from './LoadNodeParameterOptions';
export * from './NodeExecuteFunctions'; export * from './NodeExecuteFunctions';
export * from './WorkflowExecute'; export * from './WorkflowExecute';
import * as ActiveExecutions from './ActiveExecutions';
import * as NodeExecuteFunctions from './NodeExecuteFunctions'; import * as NodeExecuteFunctions from './NodeExecuteFunctions';
import * as UserSettings from './UserSettings'; import * as UserSettings from './UserSettings';
export { export {
ActiveExecutions,
NodeExecuteFunctions, NodeExecuteFunctions,
UserSettings, UserSettings,
}; };

View file

@ -160,11 +160,17 @@ export const pushConnection = mixins(
const runDataExecuted = pushData.data; const runDataExecuted = pushData.data;
if (runDataExecuted.finished !== true) { if (runDataExecuted.finished !== true) {
// There was a problem with executing the workflow // There was a problem with executing the workflow
let errorMessage = 'There was a problem executing the workflow!';
if (runDataExecuted.data.resultData.error && runDataExecuted.data.resultData.error.message) {
errorMessage = `There was a problem executing the workflow:<br /><strong>"${runDataExecuted.data.resultData.error.message}"</strong>`;
}
this.$showMessage({ this.$showMessage({
title: 'Problem executing workflow', title: 'Problem executing workflow',
message: 'There was a problem executing the workflow!', message: errorMessage,
type: 'error', type: 'error',
}); });
} else { } else {

View file

@ -106,6 +106,31 @@ export interface IDataObject {
} }
export interface IGetExecuteTriggerFunctions {
(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions;
}
export interface IGetExecuteFunctions {
(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteFunctions;
}
export interface IGetExecuteSingleFunctions {
(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, itemIndex: number, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteSingleFunctions;
}
export interface IGetExecuteHookFunctions {
(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions;
}
export interface IGetExecuteWebhookFunctions {
(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, webhookData: IWebhookData): IWebhookFunctions;
}
export interface IExecuteData { export interface IExecuteData {
data: ITaskDataConnections; data: ITaskDataConnections;
node: INode; node: INode;
@ -250,11 +275,11 @@ export interface INodeExecutionData {
export interface INodeExecuteFunctions { export interface INodeExecuteFunctions {
getExecuteTriggerFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions; getExecuteTriggerFunctions: IGetExecuteTriggerFunctions;
getExecuteFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteFunctions; getExecuteFunctions: IGetExecuteFunctions;
getExecuteSingleFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, itemIndex: number, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteSingleFunctions; getExecuteSingleFunctions: IGetExecuteSingleFunctions;
getExecuteHookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions; getExecuteHookFunctions: IGetExecuteHookFunctions;
getExecuteWebhookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, webhookData: IWebhookData): IWebhookFunctions; getExecuteWebhookFunctions: IGetExecuteWebhookFunctions;
} }
@ -452,17 +477,21 @@ export interface IWebhookResonseData {
export type WebhookResponseData = 'allEntries' | 'firstEntryJson' | 'firstEntryBinary'; export type WebhookResponseData = 'allEntries' | 'firstEntryJson' | 'firstEntryBinary';
export type WebhookResponseMode = 'onReceived' | 'lastNode'; export type WebhookResponseMode = 'onReceived' | 'lastNode';
export interface INodeTypesObject {
[key: string]: INodeType;
}
export interface INodeTypes { export interface INodeTypes {
init(nodeTypes?: INodeTypesObject): Promise<void>; nodeTypes: INodeTypeData;
init(nodeTypes?: INodeTypeData): Promise<void>;
getAll(): INodeType[]; getAll(): INodeType[];
getByName(nodeType: string): INodeType | undefined; getByName(nodeType: string): INodeType | undefined;
} }
export interface INodeTypeData {
[key: string]: {
type: INodeType;
sourcePath: string;
};
}
export interface IRun { export interface IRun {
data: IRunExecutionData; data: IRunExecutionData;
finished?: boolean; finished?: boolean;
@ -537,19 +566,17 @@ export interface IWorkflowCredentials {
} }
export interface IWorkflowExecuteHooks { export interface IWorkflowExecuteHooks {
afterExecute? (data: IRun, waitingExecutionData: IWaitingForExecution): Promise<void>; [key: string]: Array<((...args: any[]) => Promise<void>)> | undefined; // tslint:disable-line:no-any
nodeExecuteAfter?: Array<((nodeName: string, data: ITaskData) => Promise<void>)>;
nodeExecuteBefore?: Array<((nodeName: string) => Promise<void>)>;
workflowExecuteAfter?: Array<((data: IRun, newStaticData: IDataObject) => Promise<void>)>;
workflowExecuteBefore?: Array<(() => Promise<void>)>;
} }
export interface IWorkflowExecuteAdditionalData { export interface IWorkflowExecuteAdditionalData {
credentials: IWorkflowCredentials; credentials: IWorkflowCredentials;
encryptionKey: string; encryptionKey: string;
hooks?: { hooks?: IWorkflowExecuteHooks;
[key: string]: Array<((...args: any[]) => Promise<void>)> | undefined; // tslint:disable-line:no-any
nodeExecuteAfter?: Array<((executionId: string, nodeName: string, data: ITaskData) => Promise<void>)>;
nodeExecuteBefore?: Array<((nodeName: string, executionId: string) => Promise<void>)>;
workflowExecuteAfter?: Array<((data: IRun, executionId: string) => Promise<void>)>;
workflowExecuteBefore?: Array<((executionId: string) => Promise<void>)>;
};
httpResponse?: express.Response; httpResponse?: express.Response;
httpRequest?: express.Request; httpRequest?: express.Request;
timezone: string; timezone: string;

View file

@ -1,6 +1,7 @@
import { import {
IConnections, IConnections,
IGetExecuteTriggerFunctions,
INode, INode,
NodeHelpers, NodeHelpers,
INodes, INodes,
@ -954,14 +955,14 @@ export class Workflow {
* when the node has data. * when the node has data.
* *
* @param {INode} node * @param {INode} node
* @param {INodeExecuteFunctions} nodeExecuteFunctions * @param {IGetExecuteTriggerFunctions} getTriggerFunctions
* @param {IWorkflowExecuteAdditionalData} additionalData * @param {IWorkflowExecuteAdditionalData} additionalData
* @param {WorkflowExecuteMode} mode * @param {WorkflowExecuteMode} mode
* @returns {(Promise<ITriggerResponse | undefined>)} * @returns {(Promise<ITriggerResponse | undefined>)}
* @memberof Workflow * @memberof Workflow
*/ */
async runTrigger(node: INode, nodeExecuteFunctions: INodeExecuteFunctions, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): Promise<ITriggerResponse | undefined> { async runTrigger(node: INode, getTriggerFunctions: IGetExecuteTriggerFunctions, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): Promise<ITriggerResponse | undefined> {
const thisArgs = nodeExecuteFunctions.getExecuteTriggerFunctions(this, node, additionalData, mode); const triggerFunctions = getTriggerFunctions(this, node, additionalData, mode);
const nodeType = this.nodeTypes.getByName(node.type); const nodeType = this.nodeTypes.getByName(node.type);
@ -976,11 +977,11 @@ export class Workflow {
if (mode === 'manual') { if (mode === 'manual') {
// In manual mode we do not just start the trigger function we also // In manual mode we do not just start the trigger function we also
// want to be able to get informed as soon as the first data got emitted // want to be able to get informed as soon as the first data got emitted
const triggerReponse = await nodeType.trigger!.call(thisArgs); const triggerReponse = await nodeType.trigger!.call(triggerFunctions);
// Add the manual trigger response which resolves when the first time data got emitted // Add the manual trigger response which resolves when the first time data got emitted
triggerReponse!.manualTriggerResponse = new Promise((resolve) => { triggerReponse!.manualTriggerResponse = new Promise((resolve) => {
thisArgs.emit = ((resolve) => (data: INodeExecutionData[][]) => { triggerFunctions.emit = ((resolve) => (data: INodeExecutionData[][]) => {
resolve(data); resolve(data);
})(resolve); })(resolve);
}); });
@ -988,7 +989,7 @@ export class Workflow {
return triggerReponse; return triggerReponse;
} else { } else {
// In all other modes simply start the trigger // In all other modes simply start the trigger
return nodeType.trigger!.call(thisArgs); return nodeType.trigger!.call(triggerFunctions);
} }
} }
@ -1089,7 +1090,7 @@ export class Workflow {
} else if (nodeType.trigger) { } else if (nodeType.trigger) {
if (mode === 'manual') { if (mode === 'manual') {
// In manual mode start the trigger // In manual mode start the trigger
const triggerResponse = await this.runTrigger(node, nodeExecuteFunctions, additionalData, mode); const triggerResponse = await this.runTrigger(node, nodeExecuteFunctions.getExecuteTriggerFunctions, additionalData, mode);
if (triggerResponse === undefined) { if (triggerResponse === undefined) {
return null; return null;