1
0
Fork 0
mirror of synced 2024-07-15 03:05:57 +12:00

Handle js timeouts

This commit is contained in:
Adria Navarro 2024-01-24 18:03:09 +01:00
parent 73c977d6fb
commit 37033dd468
5 changed files with 111 additions and 80 deletions

View file

@ -1,6 +1,6 @@
import ivm from "isolated-vm"
import env from "./environment"
import { setJSRunner } from "@budibase/string-templates"
import { setJSRunner, JsErrorTimeout } from "@budibase/string-templates"
import { context } from "@budibase/backend-core"
import tracer from "dd-trace"
import fs from "fs"
@ -22,14 +22,15 @@ class ExecutionTimeoutError extends Error {
export function init() {
setJSRunner((js: string, ctx: Record<string, any>) => {
return tracer.trace("runJS", {}, span => {
const bbCtx = context.getCurrentContext()!
try {
const bbCtx = context.getCurrentContext()!
const isolateRefs = bbCtx.isolateRefs
if (!isolateRefs) {
const jsIsolate = new ivm.Isolate({ memoryLimit: 64 })
const jsContext = jsIsolate.createContextSync()
const isolateRefs = bbCtx.isolateRefs
if (!isolateRefs) {
const jsIsolate = new ivm.Isolate({ memoryLimit: 64 })
const jsContext = jsIsolate.createContextSync()
const injectedRequire = `const require = function(val){
const injectedRequire = `const require = function(val){
switch (val) {
case "url":
return {
@ -39,97 +40,104 @@ export function init() {
}
};`
const global = jsContext.global
global.setSync(
"urlResolveCb",
new ivm.Callback((...params: Parameters<typeof url.resolve>) =>
url.resolve(...params)
const global = jsContext.global
global.setSync(
"urlResolveCb",
new ivm.Callback((...params: Parameters<typeof url.resolve>) =>
url.resolve(...params)
)
)
)
global.setSync(
"urlParseCb",
new ivm.Callback((...params: Parameters<typeof url.parse>) =>
url.parse(...params)
global.setSync(
"urlParseCb",
new ivm.Callback((...params: Parameters<typeof url.parse>) =>
url.parse(...params)
)
)
)
const helpersModule = jsIsolate.compileModuleSync(
`${injectedRequire};${helpersSource}`
)
const helpersModule = jsIsolate.compileModuleSync(
`${injectedRequire};${helpersSource}`
)
const cryptoModule = jsIsolate.compileModuleSync(`export default {
const cryptoModule = jsIsolate.compileModuleSync(`export default {
randomUUID: cryptoRandomUUIDCb,
}`)
cryptoModule.instantiateSync(jsContext, specifier => {
throw new Error(`No imports allowed. Required: ${specifier}`)
})
cryptoModule.instantiateSync(jsContext, specifier => {
throw new Error(`No imports allowed. Required: ${specifier}`)
})
global.setSync(
"cryptoRandomUUIDCb",
new ivm.Callback(
(...params: Parameters<typeof crypto.randomUUID>) => {
return crypto.randomUUID(...params)
}
global.setSync(
"cryptoRandomUUIDCb",
new ivm.Callback(
(...params: Parameters<typeof crypto.randomUUID>) => {
return crypto.randomUUID(...params)
}
)
)
helpersModule.instantiateSync(jsContext, specifier => {
if (specifier === "crypto") {
return cryptoModule
}
throw new Error(`No imports allowed. Required: ${specifier}`)
})
for (const [key, value] of Object.entries(ctx)) {
if (key === "helpers") {
// Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource
continue
}
global.setSync(key, value)
}
bbCtx.isolateRefs = { jsContext, jsIsolate, helpersModule }
}
let { jsIsolate, jsContext, helpersModule } = bbCtx.isolateRefs!
const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS
if (perRequestLimit) {
const cpuMs = Number(jsIsolate.cpuTime) / 1e6
if (cpuMs > perRequestLimit) {
throw new ExecutionTimeoutError(
`CPU time limit exceeded (${cpuMs}ms > ${perRequestLimit}ms)`
)
}
}
const script = jsIsolate.compileModuleSync(
`import helpers from "compiled_module";${js};cb(run());`,
{}
)
helpersModule.instantiateSync(jsContext, specifier => {
if (specifier === "crypto") {
return cryptoModule
script.instantiateSync(jsContext, specifier => {
if (specifier === "compiled_module") {
return helpersModule
}
throw new Error(`No imports allowed. Required: ${specifier}`)
throw new Error(`"${specifier}" import not allowed`)
})
for (const [key, value] of Object.entries(ctx)) {
if (key === "helpers") {
// Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource
continue
}
global.setSync(key, value)
}
let result
jsContext.global.setSync(
"cb",
new ivm.Callback((value: any) => {
result = value
})
)
bbCtx.isolateRefs = { jsContext, jsIsolate, helpersModule }
}
let { jsIsolate, jsContext, helpersModule } = bbCtx.isolateRefs!
const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS
if (perRequestLimit) {
const cpuMs = Number(jsIsolate.cpuTime) / 1e6
if (cpuMs > perRequestLimit) {
throw new ExecutionTimeoutError(
`CPU time limit exceeded (${cpuMs}ms > ${perRequestLimit}ms)`
)
}
}
const script = jsIsolate.compileModuleSync(
`import helpers from "compiled_module";${js};cb(run());`,
{}
)
script.instantiateSync(jsContext, specifier => {
if (specifier === "compiled_module") {
return helpersModule
}
throw new Error(`"${specifier}" import not allowed`)
})
let result
jsContext.global.setSync(
"cb",
new ivm.Callback((value: any) => {
result = value
script.evaluateSync({
timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS,
})
)
script.evaluateSync({
timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS,
})
return result
} catch (error: any) {
if (error.message === "Script execution timed out.") {
throw new JsErrorTimeout()
}
return result
throw error
}
})
})
}

View file

@ -0,0 +1,11 @@
class JsErrorTimeout extends Error {
code = "ERR_SCRIPT_EXECUTION_TIMEOUT"
constructor() {
super()
}
}
module.exports = {
JsErrorTimeout,
}

View file

@ -35,3 +35,8 @@ if (!process.env.NO_JS) {
return vm.run(js)
})
}
const errors = require("./errors")
for (const error in errors) {
module.exports[error] = errors[error]
}

View file

@ -394,3 +394,8 @@ module.exports.convertToJS = hbs => {
}
module.exports.FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX
const errors = require("./errors")
for (const error in errors) {
module.exports[error] = errors[error]
}

View file

@ -37,3 +37,5 @@ if (process && !process.env.NO_JS) {
return vm.runInNewContext(js, context, { timeout: 1000 })
})
}
export * from "./errors.js"