| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| use super::{Health, Limits, Result, failed}; | |
| use deno_core::{JsRuntime, RuntimeOptions, v8}; | |
| use futures_util::future::LocalBoxFuture; | |
| use serde_json::Value; | |
| use std::sync::Arc; | |
| pub(super) struct Module(pub v8::Global<v8::Object>); | |
| pub(super) struct Callbacks { | |
| receiver: v8::Global<v8::Object>, | |
| functions: std::collections::BTreeMap<String, v8::Global<v8::Function>>, | |
| } | |
| pub(super) type Call = LocalBoxFuture<'static, Result<v8::Global<v8::Value>>>; | |
| fn runtime_base(health: &Arc<Health>, limits: &Limits, realm: super::Realm) -> Result<JsRuntime> { | |
| let mut extensions = vec![ | |
| deno_webidl::deno_webidl::init(), | |
| deno_web::deno_web::init( | |
| Arc::new(deno_web::BlobStore::default()), | |
| None, | |
| false, | |
| Default::default(), | |
| ), | |
| ]; | |
| if realm == super::Realm::Scoped { | |
| extensions.push(super::request::maka_requests::init()); | |
| } | |
| let mut runtime = JsRuntime::try_new(RuntimeOptions { | |
| extensions, | |
| create_params: Some(v8::CreateParams::default().heap_limits(0, limits.heap_bytes)), | |
| ..Default::default() | |
| }) | |
| .map_err(failed)?; | |
| let _ = health | |
| .isolate | |
| .set(runtime.v8_isolate().thread_safe_handle()); | |
| let watch = health.clone(); | |
| runtime.add_near_heap_limit_callback(move |current, _| { | |
| watch.fail(failed("heap budget exceeded")); | |
| current.saturating_add(16 * 1024 * 1024) | |
| }); | |
| // Pure codec/URL APIs reuse the existing Deno dependency. Do not install | |
| // ambient fetch, timers or processes: system work goes through owned SDK calls. | |
| runtime | |
| .execute_script( | |
| "maka-plugin-globals", | |
| r#"const { DOMException } = Deno.core.loadExtScript('ext:deno_web/01_dom_exception.js'); | |
| Deno.core.registerErrorBuilder('DOMExceptionInvalidCharacterError', | |
| message => new DOMException(message, 'InvalidCharacterError')); | |
| for (const [path, names] of [ | |
| ['ext:deno_web/08_text_encoding.js', ['TextEncoder', 'TextDecoder']], | |
| ['ext:deno_web/05_base64.js', ['atob']], | |
| ['ext:deno_web/00_url.js', ['URL', 'URLSearchParams']], | |
| ]) { | |
| const exports = Deno.core.loadExtScript(path); | |
| for (const name of names) Object.defineProperty(globalThis, name, { | |
| value: exports[name], configurable: true, writable: true, | |
| }); | |
| }"#, | |
| ) | |
| .map_err(failed)?; | |
| Ok(runtime) | |
| } | |
| pub(super) fn pure_runtime(health: &Arc<Health>, limits: &Limits) -> Result<JsRuntime> { | |
| let mut runtime = runtime_base(health, limits, super::Realm::Pure)?; | |
| runtime | |
| .execute_script( | |
| "maka-pure-globals", | |
| "delete globalThis.Deno; delete globalThis.__bootstrap; delete globalThis.console;", | |
| ) | |
| .map_err(failed)?; | |
| Ok(runtime) | |
| } | |
| pub(super) fn scoped_runtime(health: &Arc<Health>, limits: &Limits) -> Result<JsRuntime> { | |
| let mut runtime = runtime_base(health, limits, super::Realm::Scoped)?; | |
| super::request::initialize(&mut runtime)?; | |
| runtime | |
| .execute_script( | |
| "maka-scoped-globals", | |
| "delete globalThis.Deno; delete globalThis.__bootstrap; delete globalThis.console;", | |
| ) | |
| .map_err(failed)?; | |
| Ok(runtime) | |
| } | |
| pub(super) fn foreground_runtime(health: &Arc<Health>, limits: &Limits) -> Result<JsRuntime> { | |
| let mut runtime = runtime_base(health, limits, super::Realm::Pure)?; | |
| runtime.execute_script("maka-foreground-globals", "delete globalThis.Deno; delete globalThis.__bootstrap; delete globalThis.console; delete globalThis.queueMicrotask;") | |
| .map_err(failed)?; | |
| Ok(runtime) | |
| } | |
| pub(super) fn load(runtime: &mut JsRuntime, name: &str, source: &str) -> Result<Module> { | |
| deno_core::scope!(scope, runtime); | |
| v8::tc_scope!(scope, scope); | |
| let resource = | |
| v8::String::new(scope, name).ok_or_else(|| failed("module name allocation failed"))?; | |
| let origin = v8::ScriptOrigin::new( | |
| scope, | |
| resource.into(), | |
| 0, | |
| 0, | |
| false, | |
| 0, | |
| None, | |
| false, | |
| false, | |
| true, | |
| None, | |
| ); | |
| let text = | |
| v8::String::new(scope, source).ok_or_else(|| failed("module source allocation failed"))?; | |
| let mut source = v8::script_compiler::Source::new(text, Some(&origin)); | |
| let Some(module) = v8::script_compiler::compile_module(scope, &mut source) else { | |
| return Err(failed( | |
| scope | |
| .exception() | |
| .map(|value| value.to_rust_string_lossy(scope)) | |
| .unwrap_or_else(|| "module compilation terminated".into()), | |
| )); | |
| }; | |
| if module.get_module_requests().length() != 0 { | |
| return Err(failed( | |
| "plugin entrypoints must be prebuilt bundles without imports", | |
| )); | |
| } | |
| if module.instantiate_module(scope, no_imports) != Some(true) { | |
| return Err(failed( | |
| scope | |
| .exception() | |
| .map(|value| value.to_rust_string_lossy(scope)) | |
| .unwrap_or_else(|| "module instantiation failed".into()), | |
| )); | |
| } | |
| if module.is_graph_async() { | |
| return Err(failed( | |
| "top-level await is unsupported; initialize asynchronously in activation", | |
| )); | |
| } | |
| let Some(result) = module.evaluate(scope) else { | |
| return Err(failed( | |
| scope | |
| .exception() | |
| .map(|value| value.to_rust_string_lossy(scope)) | |
| .unwrap_or_else(|| "module evaluation terminated".into()), | |
| )); | |
| }; | |
| let promise = v8::Local::<v8::Promise>::try_from(result).map_err(failed)?; | |
| promise.mark_as_handled(); | |
| if promise.state() == v8::PromiseState::Rejected { | |
| return Err(failed(promise.result(scope).to_rust_string_lossy(scope))); | |
| } | |
| let namespace = | |
| v8::Local::<v8::Object>::try_from(module.get_module_namespace()).map_err(failed)?; | |
| Ok(Module(v8::Global::new(scope, namespace))) | |
| } | |
| fn no_imports<'s>( | |
| _: v8::Local<'s, v8::Context>, | |
| _: v8::Local<'s, v8::String>, | |
| _: v8::Local<'s, v8::FixedArray>, | |
| _: v8::Local<'s, v8::Module>, | |
| ) -> Option<v8::Local<'s, v8::Module>> { | |
| None | |
| } | |
| pub(super) fn call( | |
| runtime: &mut JsRuntime, | |
| module: &Module, | |
| path: &[String], | |
| args: &[Value], | |
| ) -> Result<Call> { | |
| let value = invoke(runtime, module, path, args)?; | |
| let resolving = runtime.resolve(value); | |
| Ok(Box::pin(async move { resolving.await.map_err(failed) })) | |
| } | |
| fn invoke( | |
| runtime: &mut JsRuntime, | |
| module: &Module, | |
| path: &[String], | |
| args: &[Value], | |
| ) -> Result<v8::Global<v8::Value>> { | |
| let value = { | |
| deno_core::scope!(scope, runtime); | |
| v8::tc_scope!(scope, scope); | |
| let mut object = v8::Local::new(scope, &module.0); | |
| for part in &path[..path.len() - 1] { | |
| let key = v8::String::new(scope, part) | |
| .ok_or_else(|| failed("export name allocation failed"))?; | |
| let value = object | |
| .get(scope, key.into()) | |
| .ok_or_else(|| failed("export lookup failed"))?; | |
| object = v8::Local::<v8::Object>::try_from(value).map_err(failed)?; | |
| } | |
| let key = v8::String::new(scope, path.last().unwrap()) | |
| .ok_or_else(|| failed("export name allocation failed"))?; | |
| let function = object | |
| .get(scope, key.into()) | |
| .ok_or_else(|| failed("export lookup failed"))?; | |
| let function = v8::Local::<v8::Function>::try_from(function) | |
| .map_err(|_| failed("export is not callable"))?; | |
| let args = args | |
| .iter() | |
| .map(|value| super::json::to_v8(scope, value).map_err(failed)) | |
| .collect::<Result<Vec<_>>>()?; | |
| let Some(value) = function.call(scope, object.into(), &args) else { | |
| return Err(failed( | |
| scope | |
| .exception() | |
| .map(|value| value.to_rust_string_lossy(scope)) | |
| .unwrap_or_else(|| "plugin call terminated".into()), | |
| )); | |
| }; | |
| v8::Global::new(scope, value) | |
| }; | |
| Ok(value) | |
| } | |
| /// No promise polling or I/O. Serialize under the same script budget: getters | |
| /// and toJSON can execute user code. | |
| pub(super) fn call_synchronous( | |
| runtime: &mut JsRuntime, | |
| module: &Module, | |
| path: &[String], | |
| args: &[Value], | |
| ) -> Result<Value> { | |
| let result = invoke(runtime, module, path, args)?; | |
| value(runtime, result) | |
| } | |
| /// Capture an actual receiver and all required functions before publishing a | |
| /// registration. Property getters/proxies execute here under the caller's budget; | |
| /// later mutation of the namespace never redirects an existing registration. | |
| pub(super) fn capture_callbacks( | |
| runtime: &mut JsRuntime, | |
| module: &Module, | |
| namespace: &[String], | |
| methods: &[&str], | |
| ) -> Result<Callbacks> { | |
| deno_core::scope!(scope, runtime); | |
| v8::tc_scope!(scope, scope); | |
| let mut receiver = v8::Local::new(scope, &module.0); | |
| for part in namespace { | |
| let key = | |
| v8::String::new(scope, part).ok_or_else(|| failed("export name allocation failed"))?; | |
| let value = receiver | |
| .get(scope, key.into()) | |
| .ok_or_else(|| failed("component lookup failed"))?; | |
| receiver = v8::Local::<v8::Object>::try_from(value).map_err(failed)?; | |
| } | |
| let mut functions = std::collections::BTreeMap::new(); | |
| for method in methods { | |
| let key = v8::String::new(scope, method) | |
| .ok_or_else(|| failed("export name allocation failed"))?; | |
| let value = receiver | |
| .get(scope, key.into()) | |
| .ok_or_else(|| failed("component callback lookup failed"))?; | |
| let function = v8::Local::<v8::Function>::try_from(value) | |
| .map_err(|_| failed(format!("component callback is not callable: {method}")))?; | |
| functions.insert((*method).into(), v8::Global::new(scope, function)); | |
| } | |
| Ok(Callbacks { | |
| receiver: v8::Global::new(scope, receiver), | |
| functions, | |
| }) | |
| } | |
| pub(super) fn call_bound_synchronous( | |
| runtime: &mut JsRuntime, | |
| callbacks: &Callbacks, | |
| method: &str, | |
| args: &[&Value], | |
| ) -> Result<Value> { | |
| let result = invoke_bound(runtime, callbacks, method, args.iter().copied(), None)?; | |
| value(runtime, result) | |
| } | |
| pub(super) fn call_bound( | |
| runtime: &mut JsRuntime, | |
| callbacks: &Callbacks, | |
| method: &str, | |
| args: &[Value], | |
| context: Option<&Arc<super::request::Owner>>, | |
| ) -> Result<Call> { | |
| let value = invoke_bound(runtime, callbacks, method, args, context)?; | |
| let resolving = runtime.resolve(value); | |
| Ok(Box::pin(async move { resolving.await.map_err(failed) })) | |
| } | |
| fn invoke_bound<'a>( | |
| runtime: &mut JsRuntime, | |
| callbacks: &Callbacks, | |
| method: &str, | |
| args: impl IntoIterator<Item = &'a Value>, | |
| context: Option<&Arc<super::request::Owner>>, | |
| ) -> Result<v8::Global<v8::Value>> { | |
| let factory = context.map(|_| { | |
| runtime | |
| .op_state() | |
| .borrow() | |
| .borrow::<super::request::Factory>() | |
| .0 | |
| .clone() | |
| }); | |
| let value = { | |
| deno_core::scope!(scope, runtime); | |
| v8::tc_scope!(scope, scope); | |
| let function = callbacks | |
| .functions | |
| .get(method) | |
| .ok_or_else(|| failed("callback was not captured"))?; | |
| let function = v8::Local::new(scope, function); | |
| let receiver = v8::Local::new(scope, &callbacks.receiver); | |
| let mut args = args | |
| .into_iter() | |
| .map(|value| super::json::to_v8(scope, value).map_err(failed)) | |
| .collect::<Result<Vec<_>>>()?; | |
| if let Some(context) = context { | |
| let native = | |
| deno_core::cppgc::make_cppgc_object(scope, super::request::Handle::new(context)); | |
| let factory = v8::Local::new(scope, factory.as_ref().expect("scoped factory")); | |
| let undefined = v8::undefined(scope).into(); | |
| let context = factory | |
| .call(scope, undefined, &[native.into()]) | |
| .ok_or_else(|| failed("request context construction failed"))?; | |
| args.push(context); | |
| } | |
| let value = function | |
| .call(scope, receiver.into(), &args) | |
| .ok_or_else(|| { | |
| failed( | |
| scope | |
| .exception() | |
| .map(|value| value.to_rust_string_lossy(scope)) | |
| .unwrap_or_else(|| "plugin call terminated".into()), | |
| ) | |
| })?; | |
| v8::Global::new(scope, value) | |
| }; | |
| Ok(value) | |
| } | |
| pub(super) fn value(runtime: &mut JsRuntime, value: v8::Global<v8::Value>) -> Result<Value> { | |
| deno_core::scope!(scope, runtime); | |
| v8::tc_scope!(scope, scope); | |
| let value = v8::Local::new(scope, value); | |
| if value.is_promise() { | |
| return Err(failed("foreground callbacks must return synchronously")); | |
| } | |
| super::json::decode(scope, value).map_err(failed) | |
| } | |
| /// Drain ready native V8 tasks and local JavaScript jobs, without waiting on | |
| /// promises, timers or operations. Foreground domains expose no system ops. | |
| pub(super) fn poll_ready_jobs(runtime: &mut JsRuntime) -> Result<()> { | |
| let mut context = std::task::Context::from_waker(std::task::Waker::noop()); | |
| match runtime.poll_event_loop(&mut context, Default::default()) { | |
| std::task::Poll::Ready(Err(error)) => Err(failed(error)), | |
| _ => Ok(()), | |
| } | |
| } |