diff --git a/packages/server/src/jsRunner/index.ts b/packages/server/src/jsRunner/index.ts index e39dab1313..93383b3955 100644 --- a/packages/server/src/jsRunner/index.ts +++ b/packages/server/src/jsRunner/index.ts @@ -1,68 +1,41 @@ -import vm from "vm" import env from "../environment" -import { setJSRunner, setOnErrorLog } from "@budibase/string-templates" -import { context, logging, timers } from "@budibase/backend-core" +import { setJSRunner, JsErrorTimeout } from "@budibase/string-templates" import tracer from "dd-trace" -import { serializeError } from "serialize-error" -type TrackerFn = (f: () => T) => T +import { IsolatedVM } from "./vm" +import { context } from "@budibase/backend-core" export function init() { - setJSRunner((js: string, ctx: vm.Context) => { + setJSRunner((js: string, ctx: Record) => { return tracer.trace("runJS", {}, span => { - const perRequestLimit = env.JS_PER_REQUEST_TIMEOUT_MS - let track: TrackerFn = f => f() - if (perRequestLimit) { - const bbCtx = tracer.trace("runJS.getCurrentContext", {}, span => - context.getCurrentContext() - ) - if (bbCtx) { - if (!bbCtx.jsExecutionTracker) { - span?.addTags({ - createdExecutionTracker: true, - }) - bbCtx.jsExecutionTracker = tracer.trace( - "runJS.createExecutionTimeTracker", - {}, - span => timers.ExecutionTimeTracker.withLimit(perRequestLimit) - ) - } - span?.addTags({ - js: { - limitMS: bbCtx.jsExecutionTracker.limitMs, - elapsedMS: bbCtx.jsExecutionTracker.elapsedMS, - }, - }) - // We call checkLimit() here to prevent paying the cost of creating - // a new VM context below when we don't need to. - tracer.trace("runJS.checkLimitAndBind", {}, span => { - bbCtx.jsExecutionTracker!.checkLimit() - track = bbCtx.jsExecutionTracker!.track.bind( - bbCtx.jsExecutionTracker - ) + try { + const bbCtx = context.getCurrentContext()! + + let { vm } = bbCtx + if (!vm) { + // Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource + const { helpers, ...ctxToPass } = ctx + + vm = new IsolatedVM({ + memoryLimit: env.JS_RUNNER_MEMORY_LIMIT, + invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS, + isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS, }) + .withContext(ctxToPass) + .withHelpers() + + bbCtx.vm = vm } - } - ctx = { - ...ctx, - alert: undefined, - setInterval: undefined, - setTimeout: undefined, - } + const result = vm.execute(js) - vm.createContext(ctx) - return track(() => - vm.runInNewContext(js, ctx, { - timeout: env.JS_PER_INVOCATION_TIMEOUT_MS, - }) - ) + return result + } catch (error: any) { + if (error.message === "Script execution timed out.") { + throw new JsErrorTimeout() + } + throw error + } }) }) - - if (env.LOG_JS_ERRORS) { - setOnErrorLog((error: Error) => { - logging.logWarn(JSON.stringify(serializeError(error))) - }) - } } diff --git a/packages/server/src/threads/query.ts b/packages/server/src/threads/query.ts index 6b11ce4759..b38dd7de6b 100644 --- a/packages/server/src/threads/query.ts +++ b/packages/server/src/threads/query.ts @@ -127,10 +127,16 @@ class QueryRunner { // transform as required if (transformer) { - const runner = new ScriptRunner(transformer, { - data: rows, - params: enrichedParameters, - }) + const runner = new ScriptRunner( + transformer, + { + data: rows, + params: enrichedParameters, + }, + { + parseBson: datasource.source === SourceName.MONGODB, + } + ) rows = runner.execute() } diff --git a/packages/server/src/utilities/scriptRunner.ts b/packages/server/src/utilities/scriptRunner.ts index fee0215d2e..72042a5791 100644 --- a/packages/server/src/utilities/scriptRunner.ts +++ b/packages/server/src/utilities/scriptRunner.ts @@ -1,28 +1,40 @@ -import fetch from "node-fetch" -import { VM, VMScript } from "vm2" +import tracer, { Span } from "dd-trace" +import env from "../environment" +import { IsolatedVM } from "../jsRunner/vm" const JS_TIMEOUT_MS = 1000 class ScriptRunner { - vm: VM - results: { out: string } - script: VMScript + private code: string + private vm: IsolatedVM - constructor(script: string, context: any) { - const code = `let fn = () => {\n${script}\n}; results.out = fn();` - this.vm = new VM({ - timeout: JS_TIMEOUT_MS, - }) - this.results = { out: "" } - this.vm.setGlobals(context) - this.vm.setGlobal("fetch", fetch) - this.vm.setGlobal("results", this.results) - this.script = new VMScript(code) + private tracerSpan: Span + + constructor(script: string, context: any, { parseBson = false } = {}) { + this.tracerSpan = tracer.startSpan("scriptRunner", { tags: { parseBson } }) + + this.code = `(() => {${script}})();` + this.vm = new IsolatedVM({ + memoryLimit: env.JS_RUNNER_MEMORY_LIMIT, + invocationTimeout: JS_TIMEOUT_MS, + }).withContext(context) + + if (parseBson && context.data) { + this.vm = this.vm.withParsingBson(context.data) + } } execute() { - this.vm.run(this.script) - return this.results.out + const result = tracer.trace( + "scriptRunner.execute", + { childOf: this.tracerSpan }, + () => { + const result = this.vm.execute(this.code) + return result + } + ) + this.tracerSpan.finish() + return result } }