Handle js timeouts
This commit is contained in:
parent
73c977d6fb
commit
37033dd468
5 changed files with 111 additions and 80 deletions
|
@ -1,6 +1,6 @@
|
||||||
import ivm from "isolated-vm"
|
import ivm from "isolated-vm"
|
||||||
import env from "./environment"
|
import env from "./environment"
|
||||||
import { setJSRunner } from "@budibase/string-templates"
|
import { setJSRunner, JsErrorTimeout } from "@budibase/string-templates"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
|
@ -22,14 +22,15 @@ class ExecutionTimeoutError extends Error {
|
||||||
export function init() {
|
export function init() {
|
||||||
setJSRunner((js: string, ctx: Record<string, any>) => {
|
setJSRunner((js: string, ctx: Record<string, any>) => {
|
||||||
return tracer.trace("runJS", {}, span => {
|
return tracer.trace("runJS", {}, span => {
|
||||||
const bbCtx = context.getCurrentContext()!
|
try {
|
||||||
|
const bbCtx = context.getCurrentContext()!
|
||||||
|
|
||||||
const isolateRefs = bbCtx.isolateRefs
|
const isolateRefs = bbCtx.isolateRefs
|
||||||
if (!isolateRefs) {
|
if (!isolateRefs) {
|
||||||
const jsIsolate = new ivm.Isolate({ memoryLimit: 64 })
|
const jsIsolate = new ivm.Isolate({ memoryLimit: 64 })
|
||||||
const jsContext = jsIsolate.createContextSync()
|
const jsContext = jsIsolate.createContextSync()
|
||||||
|
|
||||||
const injectedRequire = `const require = function(val){
|
const injectedRequire = `const require = function(val){
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case "url":
|
case "url":
|
||||||
return {
|
return {
|
||||||
|
@ -39,97 +40,104 @@ export function init() {
|
||||||
}
|
}
|
||||||
};`
|
};`
|
||||||
|
|
||||||
const global = jsContext.global
|
const global = jsContext.global
|
||||||
global.setSync(
|
global.setSync(
|
||||||
"urlResolveCb",
|
"urlResolveCb",
|
||||||
new ivm.Callback((...params: Parameters<typeof url.resolve>) =>
|
new ivm.Callback((...params: Parameters<typeof url.resolve>) =>
|
||||||
url.resolve(...params)
|
url.resolve(...params)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
global.setSync(
|
global.setSync(
|
||||||
"urlParseCb",
|
"urlParseCb",
|
||||||
new ivm.Callback((...params: Parameters<typeof url.parse>) =>
|
new ivm.Callback((...params: Parameters<typeof url.parse>) =>
|
||||||
url.parse(...params)
|
url.parse(...params)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
const helpersModule = jsIsolate.compileModuleSync(
|
const helpersModule = jsIsolate.compileModuleSync(
|
||||||
`${injectedRequire};${helpersSource}`
|
`${injectedRequire};${helpersSource}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const cryptoModule = jsIsolate.compileModuleSync(`export default {
|
const cryptoModule = jsIsolate.compileModuleSync(`export default {
|
||||||
randomUUID: cryptoRandomUUIDCb,
|
randomUUID: cryptoRandomUUIDCb,
|
||||||
}`)
|
}`)
|
||||||
cryptoModule.instantiateSync(jsContext, specifier => {
|
cryptoModule.instantiateSync(jsContext, specifier => {
|
||||||
throw new Error(`No imports allowed. Required: ${specifier}`)
|
throw new Error(`No imports allowed. Required: ${specifier}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
global.setSync(
|
global.setSync(
|
||||||
"cryptoRandomUUIDCb",
|
"cryptoRandomUUIDCb",
|
||||||
new ivm.Callback(
|
new ivm.Callback(
|
||||||
(...params: Parameters<typeof crypto.randomUUID>) => {
|
(...params: Parameters<typeof crypto.randomUUID>) => {
|
||||||
return crypto.randomUUID(...params)
|
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 => {
|
script.instantiateSync(jsContext, specifier => {
|
||||||
if (specifier === "crypto") {
|
if (specifier === "compiled_module") {
|
||||||
return cryptoModule
|
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)) {
|
let result
|
||||||
if (key === "helpers") {
|
jsContext.global.setSync(
|
||||||
// Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource
|
"cb",
|
||||||
continue
|
new ivm.Callback((value: any) => {
|
||||||
}
|
result = value
|
||||||
global.setSync(key, value)
|
})
|
||||||
}
|
)
|
||||||
|
|
||||||
bbCtx.isolateRefs = { jsContext, jsIsolate, helpersModule }
|
script.evaluateSync({
|
||||||
}
|
timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS,
|
||||||
|
|
||||||
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({
|
return result
|
||||||
timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS,
|
} catch (error: any) {
|
||||||
})
|
if (error.message === "Script execution timed out.") {
|
||||||
|
throw new JsErrorTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
throw error
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
11
packages/string-templates/src/errors.js
Normal file
11
packages/string-templates/src/errors.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class JsErrorTimeout extends Error {
|
||||||
|
code = "ERR_SCRIPT_EXECUTION_TIMEOUT"
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
JsErrorTimeout,
|
||||||
|
}
|
|
@ -35,3 +35,8 @@ if (!process.env.NO_JS) {
|
||||||
return vm.run(js)
|
return vm.run(js)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errors = require("./errors")
|
||||||
|
for (const error in errors) {
|
||||||
|
module.exports[error] = errors[error]
|
||||||
|
}
|
||||||
|
|
|
@ -394,3 +394,8 @@ module.exports.convertToJS = hbs => {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX
|
module.exports.FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX
|
||||||
|
|
||||||
|
const errors = require("./errors")
|
||||||
|
for (const error in errors) {
|
||||||
|
module.exports[error] = errors[error]
|
||||||
|
}
|
||||||
|
|
|
@ -37,3 +37,5 @@ if (process && !process.env.NO_JS) {
|
||||||
return vm.runInNewContext(js, context, { timeout: 1000 })
|
return vm.runInNewContext(js, context, { timeout: 1000 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from "./errors.js"
|
||||||
|
|
Loading…
Reference in a new issue