mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
fix(core): Don't fail task runner task if logging fails (#12401)
This commit is contained in:
@@ -139,7 +139,8 @@ describe('JsTaskRunner', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(defaultTaskRunner.makeRpcCall).toHaveBeenCalledWith(task.taskId, 'logNodeOutput', [
|
expect(defaultTaskRunner.makeRpcCall).toHaveBeenCalledWith(task.taskId, 'logNodeOutput', [
|
||||||
'Hello world!',
|
"'Hello'",
|
||||||
|
"'world!'",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -173,6 +174,44 @@ describe('JsTaskRunner', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not throw when trying to log the context object', async () => {
|
||||||
|
const task = newTaskWithSettings({
|
||||||
|
code: `
|
||||||
|
console.log(this);
|
||||||
|
return {json: {}}
|
||||||
|
`,
|
||||||
|
nodeMode: 'runOnceForAllItems',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
execTaskWithParams({
|
||||||
|
task,
|
||||||
|
taskData: newDataRequestResponse([wrapIntoJson({})]),
|
||||||
|
}),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log the context object as [[ExecutionContext]]', async () => {
|
||||||
|
const rpcCallSpy = jest.spyOn(defaultTaskRunner, 'makeRpcCall').mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const task = newTaskWithSettings({
|
||||||
|
code: `
|
||||||
|
console.log(this);
|
||||||
|
return {json: {}}
|
||||||
|
`,
|
||||||
|
nodeMode: 'runOnceForAllItems',
|
||||||
|
});
|
||||||
|
|
||||||
|
await execTaskWithParams({
|
||||||
|
task,
|
||||||
|
taskData: newDataRequestResponse([wrapIntoJson({})]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rpcCallSpy).toHaveBeenCalledWith(task.taskId, 'logNodeOutput', [
|
||||||
|
'[[ExecutionContext]]',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('built-in methods and variables available in the context', () => {
|
describe('built-in methods and variables available in the context', () => {
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ import type {
|
|||||||
EnvProviderState,
|
EnvProviderState,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
|
IWorkflowDataProxyData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import * as a from 'node:assert';
|
import * as a from 'node:assert';
|
||||||
|
import { inspect } from 'node:util';
|
||||||
import { runInNewContext, type Context } from 'node:vm';
|
import { runInNewContext, type Context } from 'node:vm';
|
||||||
|
|
||||||
import type { MainConfig } from '@/config/main-config';
|
import type { MainConfig } from '@/config/main-config';
|
||||||
@@ -79,6 +81,8 @@ type CustomConsole = {
|
|||||||
log: (...args: unknown[]) => void;
|
log: (...args: unknown[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const noOp = () => {};
|
||||||
|
|
||||||
export class JsTaskRunner extends TaskRunner {
|
export class JsTaskRunner extends TaskRunner {
|
||||||
private readonly requireResolver: RequireResolver;
|
private readonly requireResolver: RequireResolver;
|
||||||
|
|
||||||
@@ -129,29 +133,12 @@ export class JsTaskRunner extends TaskRunner {
|
|||||||
nodeTypes: this.nodeTypes,
|
nodeTypes: this.nodeTypes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const noOp = () => {};
|
|
||||||
const customConsole = {
|
|
||||||
// all except `log` are dummy methods that disregard without throwing, following existing Code node behavior
|
|
||||||
...Object.keys(console).reduce<Record<string, () => void>>((acc, name) => {
|
|
||||||
acc[name] = noOp;
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
// Send log output back to the main process. It will take care of forwarding
|
|
||||||
// it to the UI or printing to console.
|
|
||||||
log: (...args: unknown[]) => {
|
|
||||||
const logOutput = args
|
|
||||||
.map((arg) => (typeof arg === 'object' && arg !== null ? JSON.stringify(arg) : arg))
|
|
||||||
.join(' ');
|
|
||||||
void this.makeRpcCall(task.taskId, 'logNodeOutput', [logOutput]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
workflow.staticData = ObservableObject.create(workflow.staticData);
|
workflow.staticData = ObservableObject.create(workflow.staticData);
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
settings.nodeMode === 'runOnceForAllItems'
|
settings.nodeMode === 'runOnceForAllItems'
|
||||||
? await this.runForAllItems(task.taskId, settings, data, workflow, customConsole, signal)
|
? await this.runForAllItems(task.taskId, settings, data, workflow, signal)
|
||||||
: await this.runForEachItem(task.taskId, settings, data, workflow, customConsole, signal);
|
: await this.runForEachItem(task.taskId, settings, data, workflow, signal);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result,
|
result,
|
||||||
@@ -200,22 +187,14 @@ export class JsTaskRunner extends TaskRunner {
|
|||||||
settings: JSExecSettings,
|
settings: JSExecSettings,
|
||||||
data: JsTaskData,
|
data: JsTaskData,
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
customConsole: CustomConsole,
|
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
): Promise<INodeExecutionData[]> {
|
): Promise<INodeExecutionData[]> {
|
||||||
const dataProxy = this.createDataProxy(data, workflow, data.itemIndex);
|
const dataProxy = this.createDataProxy(data, workflow, data.itemIndex);
|
||||||
const inputItems = data.connectionInputData;
|
const inputItems = data.connectionInputData;
|
||||||
|
|
||||||
const context: Context = {
|
const context = this.buildContext(taskId, workflow, data.node, dataProxy, {
|
||||||
require: this.requireResolver,
|
|
||||||
module: {},
|
|
||||||
console: customConsole,
|
|
||||||
items: inputItems,
|
items: inputItems,
|
||||||
$getWorkflowStaticData: (type: 'global' | 'node') => workflow.getStaticData(type, data.node),
|
});
|
||||||
...this.getNativeVariables(),
|
|
||||||
...dataProxy,
|
|
||||||
...this.buildRpcCallObject(taskId),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await new Promise<TaskResultData['result']>((resolve, reject) => {
|
const result = await new Promise<TaskResultData['result']>((resolve, reject) => {
|
||||||
@@ -264,7 +243,6 @@ export class JsTaskRunner extends TaskRunner {
|
|||||||
settings: JSExecSettings,
|
settings: JSExecSettings,
|
||||||
data: JsTaskData,
|
data: JsTaskData,
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
customConsole: CustomConsole,
|
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
): Promise<INodeExecutionData[]> {
|
): Promise<INodeExecutionData[]> {
|
||||||
const inputItems = data.connectionInputData;
|
const inputItems = data.connectionInputData;
|
||||||
@@ -279,17 +257,7 @@ export class JsTaskRunner extends TaskRunner {
|
|||||||
for (let index = chunkStartIdx; index < chunkEndIdx; index++) {
|
for (let index = chunkStartIdx; index < chunkEndIdx; index++) {
|
||||||
const item = inputItems[index];
|
const item = inputItems[index];
|
||||||
const dataProxy = this.createDataProxy(data, workflow, index);
|
const dataProxy = this.createDataProxy(data, workflow, index);
|
||||||
const context: Context = {
|
const context = this.buildContext(taskId, workflow, data.node, dataProxy, { item });
|
||||||
require: this.requireResolver,
|
|
||||||
module: {},
|
|
||||||
console: customConsole,
|
|
||||||
item,
|
|
||||||
$getWorkflowStaticData: (type: 'global' | 'node') =>
|
|
||||||
workflow.getStaticData(type, data.node),
|
|
||||||
...this.getNativeVariables(),
|
|
||||||
...dataProxy,
|
|
||||||
...this.buildRpcCallObject(taskId),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = await new Promise<INodeExecutionData | undefined>((resolve, reject) => {
|
let result = await new Promise<INodeExecutionData | undefined>((resolve, reject) => {
|
||||||
@@ -467,4 +435,52 @@ export class JsTaskRunner extends TaskRunner {
|
|||||||
|
|
||||||
return rpcObject;
|
return rpcObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildCustomConsole(taskId: string): CustomConsole {
|
||||||
|
return {
|
||||||
|
// all except `log` are dummy methods that disregard without throwing, following existing Code node behavior
|
||||||
|
...Object.keys(console).reduce<Record<string, () => void>>((acc, name) => {
|
||||||
|
acc[name] = noOp;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
|
||||||
|
// Send log output back to the main process. It will take care of forwarding
|
||||||
|
// it to the UI or printing to console.
|
||||||
|
log: (...args: unknown[]) => {
|
||||||
|
const formattedLogArgs = args.map((arg) => inspect(arg));
|
||||||
|
void this.makeRpcCall(taskId, 'logNodeOutput', formattedLogArgs);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the 'global' context object that is passed to the script
|
||||||
|
*
|
||||||
|
* @param taskId The ID of the task. Needed for RPC calls
|
||||||
|
* @param workflow The workflow that is being executed. Needed for static data
|
||||||
|
* @param node The node that is being executed. Needed for static data
|
||||||
|
* @param dataProxy The data proxy object that provides access to built-ins
|
||||||
|
* @param additionalProperties Additional properties to add to the context
|
||||||
|
*/
|
||||||
|
private buildContext(
|
||||||
|
taskId: string,
|
||||||
|
workflow: Workflow,
|
||||||
|
node: INode,
|
||||||
|
dataProxy: IWorkflowDataProxyData,
|
||||||
|
additionalProperties: Record<string, unknown> = {},
|
||||||
|
): Context {
|
||||||
|
const context: Context = {
|
||||||
|
[inspect.custom]: () => '[[ExecutionContext]]',
|
||||||
|
require: this.requireResolver,
|
||||||
|
module: {},
|
||||||
|
console: this.buildCustomConsole(taskId),
|
||||||
|
$getWorkflowStaticData: (type: 'global' | 'node') => workflow.getStaticData(type, node),
|
||||||
|
...this.getNativeVariables(),
|
||||||
|
...dataProxy,
|
||||||
|
...this.buildRpcCallObject(taskId),
|
||||||
|
...additionalProperties,
|
||||||
|
};
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -452,15 +452,15 @@ export abstract class TaskRunner extends EventEmitter {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.send({
|
|
||||||
type: 'runner:rpc',
|
|
||||||
callId,
|
|
||||||
taskId,
|
|
||||||
name,
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.send({
|
||||||
|
type: 'runner:rpc',
|
||||||
|
callId,
|
||||||
|
taskId,
|
||||||
|
name,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
const returnValue = await dataPromise;
|
const returnValue = await dataPromise;
|
||||||
|
|
||||||
return isSerializedBuffer(returnValue) ? toBuffer(returnValue) : returnValue;
|
return isSerializedBuffer(returnValue) ? toBuffer(returnValue) : returnValue;
|
||||||
|
|||||||
Reference in New Issue
Block a user