Use isolated-vm
This commit is contained in:
parent
2fe6dbe8a5
commit
6f6100e7a2
|
@ -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 = <T>(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<string, any>) => {
|
||||
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)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue