mirror of
https://github.com/gorules/zen.git
synced 2026-10-07 16:02:26 +00:00
feat: replace v8 with quickjs (#119)
* feat: replace v8 with quickjs * fix format; fix pipelines * fix: workflow * fix: remove incorrect target in node windows pipeline
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
use itertools::Itertools;
|
||||
use rquickjs::{Ctx, Error as QError, FromJs, Type, Value as QValue};
|
||||
use serde_json::{json, Map, Number, Value};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct JsValue(pub(crate) Value);
|
||||
|
||||
impl<'js> FromJs<'js> for JsValue {
|
||||
fn from_js(ctx: &Ctx<'js>, v: QValue<'js>) -> rquickjs::Result<Self> {
|
||||
let computed_value = match v.type_of() {
|
||||
Type::Uninitialized | Type::Undefined | Type::Null => Value::Null,
|
||||
Type::Bool => Value::Bool(
|
||||
v.as_bool()
|
||||
.ok_or_else(|| QError::new_from_js("bool", "bool"))?,
|
||||
),
|
||||
Type::Int => Value::Number(Number::from(
|
||||
v.as_int()
|
||||
.ok_or_else(|| QError::new_from_js("int", "number"))?,
|
||||
)),
|
||||
Type::BigInt => Value::Number(Number::from(
|
||||
v.as_big_int()
|
||||
.map(|b| b.clone().to_i64().ok())
|
||||
.flatten()
|
||||
.ok_or_else(|| QError::new_from_js("bigint", "number"))?,
|
||||
)),
|
||||
Type::Float => Value::Number(
|
||||
v.as_float()
|
||||
.map(|n| Number::from_f64(n))
|
||||
.flatten()
|
||||
.ok_or_else(|| QError::new_from_js("float", "number"))?,
|
||||
),
|
||||
Type::String => Value::String(
|
||||
v.as_string()
|
||||
.map(|s| s.to_string().ok())
|
||||
.flatten()
|
||||
.ok_or_else(|| QError::new_from_js("string", "string"))?,
|
||||
),
|
||||
Type::Array => {
|
||||
let arr = v
|
||||
.as_array()
|
||||
.ok_or_else(|| QError::new_from_js("array", "array"))?;
|
||||
|
||||
let js_arr: Vec<Value> = arr
|
||||
.iter::<QValue>()
|
||||
.map_ok(|n| JsValue::from_js(ctx, n.clone()).map(|js_val| js_val.0))
|
||||
.flatten()
|
||||
.try_collect()
|
||||
.map_err(|_| QError::new_from_js("array", "array"))?;
|
||||
|
||||
Value::Array(js_arr)
|
||||
}
|
||||
Type::Object => {
|
||||
let object = v
|
||||
.as_object()
|
||||
.ok_or_else(|| QError::new_from_js("object", "object"))?;
|
||||
|
||||
let js_object: Map<String, Value> = object
|
||||
.props::<String, QValue>()
|
||||
.map_ok(|(key, value)| {
|
||||
JsValue::from_js(ctx, value.clone()).map(|js_val| (key, js_val.0))
|
||||
})
|
||||
.flatten()
|
||||
.try_collect()
|
||||
.map_err(|_| QError::new_from_js("object", "object"))?;
|
||||
|
||||
Value::Object(js_object)
|
||||
}
|
||||
Type::Exception => {
|
||||
let exception = v
|
||||
.as_exception()
|
||||
.ok_or_else(|| QError::new_from_js("exception", "object"))?;
|
||||
|
||||
let message = exception
|
||||
.message()
|
||||
.ok_or_else(|| QError::new_from_js("exception", "object"))?;
|
||||
let description = exception.to_string();
|
||||
|
||||
json!({ "message": message, "description": description })
|
||||
}
|
||||
Type::Function => json!("[Function]"),
|
||||
Type::Module => json!("[Module]"),
|
||||
Type::Constructor => json!("[Constructor]"),
|
||||
Type::Symbol => json!("[Symbol]"),
|
||||
Type::Unknown => json!("[Unknown]"),
|
||||
};
|
||||
|
||||
Ok(JsValue(computed_value))
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use std::time::Duration;
|
||||
use rquickjs::Runtime;
|
||||
use serde_json::json;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::handler::function::script::{EvaluateResponse, Script};
|
||||
use crate::handler::function::script::Script;
|
||||
use crate::handler::node::{NodeRequest, NodeResponse, NodeResult};
|
||||
use crate::model::DecisionNodeKind;
|
||||
|
||||
mod js_value;
|
||||
pub(crate) mod runtime;
|
||||
mod script;
|
||||
mod vm;
|
||||
|
||||
async fn evaluate(source: &str, args: &Value) -> anyhow::Result<EvaluateResponse> {
|
||||
let mut script = Script::new().with_timeout(Duration::from_millis(50));
|
||||
script.call(source, args).await
|
||||
}
|
||||
|
||||
pub struct FunctionHandler {
|
||||
trace: bool,
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
static MAX_DURATION: Duration = Duration::from_millis(500);
|
||||
|
||||
impl FunctionHandler {
|
||||
pub fn new(trace: bool) -> Self {
|
||||
Self { trace }
|
||||
pub fn new(trace: bool, runtime: Runtime) -> Self {
|
||||
Self { trace, runtime }
|
||||
}
|
||||
|
||||
pub async fn handle(&self, request: &NodeRequest<'_>) -> NodeResult {
|
||||
@@ -30,11 +30,19 @@ impl FunctionHandler {
|
||||
_ => Err(anyhow!("Unexpected node type")),
|
||||
}?;
|
||||
|
||||
let result = evaluate(content.as_str(), &request.input).await?;
|
||||
let start = Instant::now();
|
||||
let interrupt_handler = Box::new(move || start.elapsed() > MAX_DURATION);
|
||||
self.runtime.set_interrupt_handler(Some(interrupt_handler));
|
||||
|
||||
let mut script = Script::new(self.runtime.clone());
|
||||
let result_response = script.call(content, &request.input).await;
|
||||
|
||||
self.runtime.set_interrupt_handler(None);
|
||||
|
||||
let response = result_response?;
|
||||
Ok(NodeResponse {
|
||||
output: result.output,
|
||||
trace_data: self.trace.then(|| json!({ "log": result.log })),
|
||||
output: response.output,
|
||||
trace_data: self.trace.then(|| json!({ "log": response.log })),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use anyhow::Context;
|
||||
use rquickjs::loader::Bundle;
|
||||
use rquickjs::{embed, Runtime};
|
||||
|
||||
static JS_BUNDLE: Bundle = embed! {
|
||||
"dayjs": "js/dayjs.js",
|
||||
"big": "js/big.js",
|
||||
"internals": "js/internals.js"
|
||||
};
|
||||
|
||||
pub(crate) fn create_runtime() -> anyhow::Result<Runtime> {
|
||||
let runtime = Runtime::new().context("Failed to create runtime")?;
|
||||
runtime.set_loader(JS_BUNDLE, JS_BUNDLE);
|
||||
|
||||
Ok(runtime)
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::fmt::Debug;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::Context as _;
|
||||
use rquickjs::{Context, Ctx, Error as QError, FromJs, Runtime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::handler::function::vm::BASE_VM;
|
||||
use crate::handler::function::js_value::JsValue;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -16,84 +15,47 @@ pub struct EvaluateResponse {
|
||||
}
|
||||
|
||||
pub struct Script {
|
||||
isolate: v8::OwnedIsolate,
|
||||
timeout: Option<Duration>,
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
isolate: v8::Isolate::new(
|
||||
v8::CreateParams::default().snapshot_blob(BASE_VM.as_slice()),
|
||||
),
|
||||
timeout: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
assert!(timeout > Duration::ZERO);
|
||||
|
||||
self.timeout = Some(timeout);
|
||||
self
|
||||
pub fn new(runtime: Runtime) -> Self {
|
||||
Self { runtime }
|
||||
}
|
||||
|
||||
pub async fn call<P>(&mut self, source: &str, args: &P) -> anyhow::Result<EvaluateResponse>
|
||||
where
|
||||
P: Serialize,
|
||||
{
|
||||
let handle = self.isolate.thread_safe_handle();
|
||||
let runtime = &self.runtime;
|
||||
let context = Context::full(&runtime).context("Failed to create context")?;
|
||||
|
||||
let args_str =
|
||||
serde_json::to_string(args).context("Failed to serialize function arguments")?;
|
||||
|
||||
let js_code_source = format!("const now = Date.now(); main({});", args_str);
|
||||
let json_response = context.with(|ctx| -> anyhow::Result<String> {
|
||||
let _ = ctx
|
||||
.clone()
|
||||
.compile("main", "import 'internals'; globalThis.now = Date.now();")
|
||||
.map_err(|e| map_js_error(&ctx, e))?;
|
||||
|
||||
let handle_scope = &mut v8::HandleScope::new(&mut self.isolate);
|
||||
let context = v8::Context::new(handle_scope);
|
||||
let scope = &mut v8::ContextScope::new(handle_scope, context);
|
||||
let tc_scope = &mut v8::TryCatch::new(scope);
|
||||
let _ = ctx
|
||||
.globals()
|
||||
.set("log", Vec::<isize>::new())
|
||||
.map_err(|e| map_js_error(&ctx, e))?;
|
||||
|
||||
let src = v8::String::new(tc_scope, source).context("Failed to compile source code")?;
|
||||
ctx.eval::<String, _>(format!("{source};main({args_str})"))
|
||||
.map_err(|e| map_js_error(&ctx, e))
|
||||
})?;
|
||||
|
||||
let js_src = v8::String::new(tc_scope, js_code_source.as_str())
|
||||
.context("Failed to compile source code")?;
|
||||
|
||||
if let Some(timeout) = self.timeout {
|
||||
thread::spawn(move || {
|
||||
thread::sleep(timeout);
|
||||
handle.terminate_execution();
|
||||
});
|
||||
}
|
||||
|
||||
let Some(src_script) = v8::Script::compile(tc_scope, src, None) else {
|
||||
let exception = tc_scope.exception().context("Failed to load script")?;
|
||||
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
|
||||
};
|
||||
|
||||
if let None = src_script.run(tc_scope) {
|
||||
let exception = tc_scope.exception().unwrap();
|
||||
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
|
||||
}
|
||||
|
||||
let Some(js_script) = v8::Script::compile(tc_scope, js_src, None) else {
|
||||
let exception = tc_scope.exception().context("Failed to load script")?;
|
||||
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
|
||||
};
|
||||
|
||||
let Some(result) = js_script.run(tc_scope) else {
|
||||
if tc_scope.has_terminated() {
|
||||
return Err(anyhow!("Timeout exceeded"));
|
||||
}
|
||||
|
||||
let exception = tc_scope
|
||||
.exception()
|
||||
.context("Failed to run loaded script")?;
|
||||
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
|
||||
};
|
||||
|
||||
let result_string: String =
|
||||
serde_v8::from_v8(tc_scope, result).context("Failed to parse function result")?;
|
||||
|
||||
serde_json::from_str(result_string.as_str()).context("Failed to parse function result")
|
||||
serde_json::from_str(json_response.as_str()).context("Failed to parse function result")
|
||||
}
|
||||
}
|
||||
|
||||
fn map_js_error(ctx: &Ctx, e: QError) -> anyhow::Error {
|
||||
let error = JsValue::from_js(&ctx, ctx.catch())
|
||||
.map(|v| v.0)
|
||||
.unwrap_or(Value::String(e.to_string()));
|
||||
|
||||
anyhow::Error::msg(error.to_string())
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,22 +0,0 @@
|
||||
const log = [];
|
||||
|
||||
const console = {
|
||||
log: (...args) => {
|
||||
try {
|
||||
log.push({
|
||||
msSinceRun: Date.now() - now,
|
||||
lines: args.map(a => JSON.stringify(a))
|
||||
});
|
||||
} catch (e) {
|
||||
log.push({
|
||||
msSinceRun: Date.now() - now,
|
||||
lines: [JSON.stringify('failed to parse logging line')]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const main = (input) => JSON.stringify({
|
||||
output: handler(input, {moment: __GLOBAL__DAYJS, dayjs: __GLOBAL__DAYJS, Big: Big, env: __GLOBAL__ENV}),
|
||||
log,
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use v8::FunctionCodeHandling;
|
||||
|
||||
const EXPOSED_PREFIX: &'static str = "ZEN_EXPOSED_";
|
||||
|
||||
pub static BASE_VM: Lazy<Vec<u8>> = Lazy::new(|| {
|
||||
let platform = v8::new_default_platform(0, false).make_shared();
|
||||
v8::V8::initialize_platform(platform);
|
||||
v8::V8::initialize();
|
||||
|
||||
let env_src_string = {
|
||||
let mut env_vars = HashMap::new();
|
||||
env::vars().for_each(|(key, value)| {
|
||||
if let Some(mod_key) = key.strip_prefix(EXPOSED_PREFIX) {
|
||||
env_vars.insert(mod_key.to_string(), value);
|
||||
}
|
||||
});
|
||||
|
||||
format!(
|
||||
"const __GLOBAL__ENV = {}",
|
||||
serde_json::to_string(&env_vars).unwrap()
|
||||
)
|
||||
};
|
||||
|
||||
let mut isolate = v8::Isolate::snapshot_creator(Default::default());
|
||||
|
||||
{
|
||||
let handle_scope = &mut v8::HandleScope::new(&mut isolate);
|
||||
let context = v8::Context::new(handle_scope);
|
||||
|
||||
let scope = &mut v8::ContextScope::new(handle_scope, context);
|
||||
|
||||
let dayjs_src = v8::String::new(scope, include_str!("scripts/dayjs.js")).unwrap();
|
||||
let bigjs_src = v8::String::new(scope, include_str!("scripts/big.js")).unwrap();
|
||||
let internal_src = v8::String::new(scope, include_str!("scripts/internals.js")).unwrap();
|
||||
let env_src = v8::String::new(scope, env_src_string.as_str()).unwrap();
|
||||
|
||||
v8::Script::compile(scope, dayjs_src, None)
|
||||
.unwrap()
|
||||
.run(scope);
|
||||
v8::Script::compile(scope, bigjs_src, None)
|
||||
.unwrap()
|
||||
.run(scope);
|
||||
v8::Script::compile(scope, internal_src, None)
|
||||
.unwrap()
|
||||
.run(scope);
|
||||
v8::Script::compile(scope, env_src, None)
|
||||
.unwrap()
|
||||
.run(scope);
|
||||
|
||||
scope.set_default_context(context);
|
||||
}
|
||||
|
||||
isolate
|
||||
.create_blob(FunctionCodeHandling::Keep)
|
||||
.unwrap()
|
||||
.to_vec()
|
||||
});
|
||||
Reference in New Issue
Block a user