Merge pull request #13075 from Budibase/fix-context-in-js
Fix re-used context in JS runner.
This commit is contained in:
commit
40aff20af9
|
@ -3,9 +3,10 @@ import { IsolatedVM } from "../../jsRunner/vm"
|
||||||
|
|
||||||
export async function execute(ctx: Ctx) {
|
export async function execute(ctx: Ctx) {
|
||||||
const { script, context } = ctx.request.body
|
const { script, context } = ctx.request.body
|
||||||
const runner = new IsolatedVM().withContext(context)
|
const vm = new IsolatedVM()
|
||||||
|
const result = vm.withContext(context, () =>
|
||||||
const result = runner.execute(`(function(){\n${script}\n})();`)
|
vm.execute(`(function(){\n${script}\n})();`)
|
||||||
|
)
|
||||||
ctx.body = result
|
ctx.body = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2135,5 +2135,48 @@ describe.each([
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should not carry over context between formulas", async () => {
|
||||||
|
const js = Buffer.from(`return $("[text]");`).toString("base64")
|
||||||
|
const table = await config.createTable({
|
||||||
|
name: "table",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
text: {
|
||||||
|
name: "text",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
formula: {
|
||||||
|
name: "formula",
|
||||||
|
type: FieldType.FORMULA,
|
||||||
|
formula: `{{ js "${js}"}}`,
|
||||||
|
formulaType: FormulaType.DYNAMIC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await config.api.row.save(table._id!, { text: `foo${i}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows).toHaveLength(10)
|
||||||
|
|
||||||
|
const formulaValues = rows.map(r => r.formula)
|
||||||
|
expect(formulaValues).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"foo0",
|
||||||
|
"foo1",
|
||||||
|
"foo2",
|
||||||
|
"foo3",
|
||||||
|
"foo4",
|
||||||
|
"foo5",
|
||||||
|
"foo6",
|
||||||
|
"foo7",
|
||||||
|
"foo8",
|
||||||
|
"foo9",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,25 +23,20 @@ export function init() {
|
||||||
try {
|
try {
|
||||||
const bbCtx = context.getCurrentContext()
|
const bbCtx = context.getCurrentContext()
|
||||||
|
|
||||||
let vm = bbCtx?.vm
|
const vm = bbCtx?.vm
|
||||||
if (!vm) {
|
? bbCtx.vm
|
||||||
// Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource
|
: new IsolatedVM({
|
||||||
const { helpers, ...ctxToPass } = ctx
|
|
||||||
|
|
||||||
vm = new IsolatedVM({
|
|
||||||
memoryLimit: env.JS_RUNNER_MEMORY_LIMIT,
|
memoryLimit: env.JS_RUNNER_MEMORY_LIMIT,
|
||||||
invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS,
|
invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS,
|
||||||
isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS,
|
isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS,
|
||||||
})
|
}).withHelpers()
|
||||||
.withContext(ctxToPass)
|
|
||||||
.withHelpers()
|
|
||||||
|
|
||||||
if (bbCtx) {
|
if (bbCtx) {
|
||||||
// If we have a context, we want to persist it to reuse the isolate
|
// If we have a context, we want to persist it to reuse the isolate
|
||||||
bbCtx.vm = vm
|
bbCtx.vm = vm
|
||||||
}
|
}
|
||||||
}
|
const { helpers, ...rest } = ctx
|
||||||
return vm.execute(js)
|
return vm.withContext(rest, () => vm.execute(js))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message === "Script execution timed out.") {
|
if (error.message === "Script execution timed out.") {
|
||||||
throw new JsErrorTimeout()
|
throw new JsErrorTimeout()
|
||||||
|
|
|
@ -15,6 +15,17 @@ export class BuiltInVM implements VM {
|
||||||
this.span = span
|
this.span = span
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withContext<T>(context: Record<string, any>, executeWithContext: () => T): T {
|
||||||
|
this.ctx = vm.createContext(context)
|
||||||
|
try {
|
||||||
|
return executeWithContext()
|
||||||
|
} finally {
|
||||||
|
for (const key in context) {
|
||||||
|
delete this.ctx[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
execute(code: string) {
|
execute(code: string) {
|
||||||
const perRequestLimit = env.JS_PER_REQUEST_TIMEOUT_MS
|
const perRequestLimit = env.JS_PER_REQUEST_TIMEOUT_MS
|
||||||
let track: TrackerFn = f => f()
|
let track: TrackerFn = f => f()
|
||||||
|
|
|
@ -97,10 +97,14 @@ export class IsolatedVM implements VM {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(context: Record<string, any>) {
|
withContext<T>(context: Record<string, any>, executeWithContext: () => T) {
|
||||||
this.addToContext(context)
|
this.addToContext(context)
|
||||||
|
|
||||||
return this
|
try {
|
||||||
|
return executeWithContext()
|
||||||
|
} finally {
|
||||||
|
this.removeFromContext(Object.keys(context))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withParsingBson(data: any) {
|
withParsingBson(data: any) {
|
||||||
|
@ -224,6 +228,12 @@ export class IsolatedVM implements VM {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private removeFromContext(keys: string[]) {
|
||||||
|
for (let key of keys) {
|
||||||
|
this.jail.deleteSync(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getFromContext(key: string) {
|
private getFromContext(key: string) {
|
||||||
const ref = this.vm.global.getSync(key, { reference: true })
|
const ref = this.vm.global.getSync(key, { reference: true })
|
||||||
const result = ref.copySync()
|
const result = ref.copySync()
|
||||||
|
|
|
@ -7,16 +7,26 @@ export class VM2 implements VM {
|
||||||
vm: vm2.VM
|
vm: vm2.VM
|
||||||
results: { out: string }
|
results: { out: string }
|
||||||
|
|
||||||
constructor(context: any) {
|
constructor() {
|
||||||
this.vm = new vm2.VM({
|
this.vm = new vm2.VM({
|
||||||
timeout: JS_TIMEOUT_MS,
|
timeout: JS_TIMEOUT_MS,
|
||||||
})
|
})
|
||||||
this.results = { out: "" }
|
this.results = { out: "" }
|
||||||
this.vm.setGlobals(context)
|
|
||||||
this.vm.setGlobal("fetch", fetch)
|
this.vm.setGlobal("fetch", fetch)
|
||||||
this.vm.setGlobal("results", this.results)
|
this.vm.setGlobal("results", this.results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withContext<T>(context: Record<string, any>, executeWithContext: () => T): T {
|
||||||
|
this.vm.setGlobals(context)
|
||||||
|
try {
|
||||||
|
return executeWithContext()
|
||||||
|
} finally {
|
||||||
|
for (const key in context) {
|
||||||
|
this.vm.setGlobal(key, undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
execute(script: string) {
|
execute(script: string) {
|
||||||
const code = `let fn = () => {\n${script}\n}; results.out = fn();`
|
const code = `let fn = () => {\n${script}\n}; results.out = fn();`
|
||||||
const vmScript = new vm2.VMScript(code)
|
const vmScript = new vm2.VMScript(code)
|
||||||
|
|
|
@ -131,24 +131,21 @@ class QueryRunner {
|
||||||
if (transformer) {
|
if (transformer) {
|
||||||
let runner: VM
|
let runner: VM
|
||||||
if (!USE_ISOLATED_VM) {
|
if (!USE_ISOLATED_VM) {
|
||||||
runner = new VM2({
|
runner = new VM2()
|
||||||
data: rows,
|
|
||||||
params: enrichedParameters,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
transformer = `(function(){\n${transformer}\n})();`
|
transformer = `(function(){\n${transformer}\n})();`
|
||||||
let isolatedVm = new IsolatedVM().withContext({
|
let vm = new IsolatedVM()
|
||||||
|
if (datasource.source === SourceName.MONGODB) {
|
||||||
|
vm = vm.withParsingBson(rows)
|
||||||
|
}
|
||||||
|
runner = vm
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
data: rows,
|
data: rows,
|
||||||
params: enrichedParameters,
|
params: enrichedParameters,
|
||||||
})
|
|
||||||
if (datasource.source === SourceName.MONGODB) {
|
|
||||||
isolatedVm = isolatedVm.withParsingBson(rows)
|
|
||||||
}
|
}
|
||||||
|
rows = runner.withContext(ctx, () => runner.execute(transformer))
|
||||||
runner = isolatedVm
|
|
||||||
}
|
|
||||||
|
|
||||||
rows = runner.execute(transformer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the request fails we retry once, invalidating the cached value
|
// if the request fails we retry once, invalidating the cached value
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export interface VM {
|
export interface VM {
|
||||||
execute(code: string): any
|
execute(code: string): any
|
||||||
|
withContext<T>(context: Record<string, any>, executeWithContext: () => T): T
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue