fix(Code Node): Consistent redirection of stdout for JS and Python sandboxes (#6818)

Co-authored-by: Marcus <marcus@n8n.io>
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-08-01 17:47:43 +02:00
committed by GitHub
parent 34df8b6238
commit f718c2291f
5 changed files with 28 additions and 31 deletions

View File

@@ -92,8 +92,9 @@ export class Code implements INodeType {
const nodeMode = this.getNodeParameter('mode', 0) as CodeExecutionMode; const nodeMode = this.getNodeParameter('mode', 0) as CodeExecutionMode;
const workflowMode = this.getMode(); const workflowMode = this.getMode();
const node = this.getNode();
const language: CodeNodeEditorLanguage = const language: CodeNodeEditorLanguage =
this.getNode()?.typeVersion === 2 node.typeVersion === 2
? (this.getNodeParameter('language', 0) as CodeNodeEditorLanguage) ? (this.getNodeParameter('language', 0) as CodeNodeEditorLanguage)
: 'javaScript'; : 'javaScript';
const codeParameterName = language === 'python' ? 'pythonCode' : 'jsCode'; const codeParameterName = language === 'python' ? 'pythonCode' : 'jsCode';
@@ -107,16 +108,16 @@ export class Code implements INodeType {
context.item = context.$input.item; context.item = context.$input.item;
} }
if (language === 'python') { const Sandbox = language === 'python' ? PythonSandbox : JavaScriptSandbox;
context.printOverwrite = workflowMode === 'manual' ? this.sendMessageToUI : null; const sandbox = new Sandbox(context, code, index, this.helpers);
return new PythonSandbox(context, code, index, this.helpers); sandbox.on(
} else { 'output',
const sandbox = new JavaScriptSandbox(context, code, index, workflowMode, this.helpers); workflowMode === 'manual'
if (workflowMode === 'manual') { ? this.sendMessageToUI
sandbox.vm.on('console.log', this.sendMessageToUI); : (...args) =>
} console.log(`[Workflow "${this.getWorkflow().id}"][Node "${node.name}"]`, ...args),
return sandbox; );
} return sandbox;
}; };
// ---------------------------------- // ----------------------------------

View File

@@ -1,6 +1,5 @@
import type { NodeVMOptions } from 'vm2';
import { NodeVM, makeResolverFromLegacyOptions } from 'vm2'; import { NodeVM, makeResolverFromLegacyOptions } from 'vm2';
import type { IExecuteFunctions, INodeExecutionData, WorkflowExecuteMode } from 'n8n-workflow'; import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { ValidationError } from './ValidationError'; import { ValidationError } from './ValidationError';
import { ExecutionError } from './ExecutionError'; import { ExecutionError } from './ExecutionError';
@@ -20,23 +19,13 @@ export const vmResolver = makeResolverFromLegacyOptions({
builtin: builtIn?.split(',') ?? [], builtin: builtIn?.split(',') ?? [],
}); });
const getSandboxOptions = (
context: SandboxContext,
workflowMode: WorkflowExecuteMode,
): NodeVMOptions => ({
console: workflowMode === 'manual' ? 'redirect' : 'inherit',
sandbox: context,
require: vmResolver,
});
export class JavaScriptSandbox extends Sandbox { export class JavaScriptSandbox extends Sandbox {
readonly vm: NodeVM; private readonly vm: NodeVM;
constructor( constructor(
context: SandboxContext, context: SandboxContext,
private jsCode: string, private jsCode: string,
itemIndex: number | undefined, itemIndex: number | undefined,
workflowMode: WorkflowExecuteMode,
helpers: IExecuteFunctions['helpers'], helpers: IExecuteFunctions['helpers'],
) { ) {
super( super(
@@ -49,7 +38,13 @@ export class JavaScriptSandbox extends Sandbox {
itemIndex, itemIndex,
helpers, helpers,
); );
this.vm = new NodeVM(getSandboxOptions(context, workflowMode)); this.vm = new NodeVM({
console: 'redirect',
sandbox: context,
require: vmResolver,
});
this.vm.on('console.log', (...args: unknown[]) => this.emit('output', ...args));
} }
async runCodeAllItems(): Promise<INodeExecutionData[]> { async runCodeAllItems(): Promise<INodeExecutionData[]> {

View File

@@ -67,10 +67,7 @@ export class PythonSandbox extends Sandbox {
globalsDict.set(key, value); globalsDict.set(key, value);
} }
await pyodide.runPythonAsync(` pyodide.setStdout({ batched: (str) => this.emit('output', str) });
if 'printOverwrite' in globals():
print = printOverwrite
`);
const runCode = ` const runCode = `
async def __main(): async def __main():

View File

@@ -1,3 +1,4 @@
import { EventEmitter } from 'events';
import { ValidationError } from './ValidationError'; import { ValidationError } from './ValidationError';
import { isObject } from './utils'; import { isObject } from './utils';
@@ -31,12 +32,14 @@ export function getSandboxContext(this: IExecuteFunctions, index: number): Sandb
}; };
} }
export abstract class Sandbox { export abstract class Sandbox extends EventEmitter {
constructor( constructor(
private textKeys: SandboxTextKeys, private textKeys: SandboxTextKeys,
protected itemIndex: number | undefined, protected itemIndex: number | undefined,
protected helpers: IExecuteFunctions['helpers'], protected helpers: IExecuteFunctions['helpers'],
) {} ) {
super();
}
abstract runCodeAllItems(): Promise<INodeExecutionData[]>; abstract runCodeAllItems(): Promise<INodeExecutionData[]>;

View File

@@ -20,6 +20,7 @@ describe('Test Code Node', () => {
describe('Code Node unit test', () => { describe('Code Node unit test', () => {
const node = new Code(); const node = new Code();
const thisArg = mock<IExecuteFunctions>({ const thisArg = mock<IExecuteFunctions>({
getNode: () => mock(),
helpers: { normalizeItems }, helpers: { normalizeItems },
prepareOutputData: NodeHelpers.prepareOutputData, prepareOutputData: NodeHelpers.prepareOutputData,
}); });