use crate::dom::Dom;
use crate::js_runtime::extensions::audio_ext::audio_extension;
use crate::js_runtime::extensions::canvas_ext::{canvas_extension, CanvasState};
use crate::js_runtime::extensions::console_ext::console_extension;
use crate::js_runtime::extensions::crypto_ext::crypto_extension;
use crate::js_runtime::extensions::dom_ext::dom_extension;
use crate::js_runtime::extensions::fetch_ext::{fetch_extension, FetchState};
use crate::js_runtime::extensions::input_ext::input_extension;
use crate::js_runtime::extensions::layout_ext::layout_extension;
use crate::js_runtime::extensions::nav_ext::{nav_extension, NavSignal};
use crate::js_runtime::extensions::perf_ext::{perf_extension, PerfState};
use crate::js_runtime::extensions::sse_ext::{sse_extension, SseState};
use crate::js_runtime::extensions::stealth_ext::{stealth_extension, StealthState};
use crate::js_runtime::extensions::timer_ext::{timer_extension, TimerState};
use crate::js_runtime::extensions::webgl_ext::{webgl_extension, WebGLState};
use crate::js_runtime::extensions::websocket_ext::{websocket_extension, WebSocketState};
use crate::js_runtime::extensions::worker_ext::worker_extension;
use crate::js_runtime::state::DomState;
use crate::stealth::StealthProfile;
use deno_core::{v8, JsRuntime, RuntimeOptions, SharedArrayBufferStore};
use std::collections::HashMap;
#[derive(Default)]
pub struct BrowserRuntimeOptions {
pub base_url: Option<url::Url>,
pub stealth_profile: Option<StealthProfile>,
pub stylesheets: Vec<String>,
pub init_scripts: Vec<String>,
pub storage: Option<HashMap<String, HashMap<String, String>>>,
pub startup_snapshot: Option<&'static [u8]>,
pub cross_origin_isolated: bool,
pub is_secure_context: bool,
}
pub const DEFAULT_HEAP_MAX_MB: usize = 4096;
pub const DEFAULT_HEAP_INITIAL_MB: usize = 1024;
fn heap_limits() -> (usize, usize) {
fn mb_from_env(key: &str, default_mb: usize) -> usize {
match std::env::var(key) {
Ok(raw) => match raw.trim().parse::<usize>() {
Ok(mb) if mb > 0 => mb,
_ => {
tracing::warn!(
env = key,
value = %raw,
default_mb,
"ignoring unparseable/zero heap limit; using default"
);
default_mb
}
},
Err(_) => default_mb,
}
}
let max_mb = mb_from_env("BROWSER_OXIDE_HEAP_MAX_MB", DEFAULT_HEAP_MAX_MB);
let initial_mb = mb_from_env("BROWSER_OXIDE_HEAP_INITIAL_MB", DEFAULT_HEAP_INITIAL_MB);
let initial_mb = initial_mb.min(max_mb);
const MIB: usize = 1024 * 1024;
(initial_mb * MIB, max_mb * MIB)
}
fn ensure_tokio_context() -> Option<tokio::runtime::EnterGuard<'static>> {
if tokio::runtime::Handle::try_current().is_ok() {
return None;
}
static FALLBACK_RT: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new();
let rt = FALLBACK_RT.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_time()
.thread_name("browser-oxide-v8-delayed")
.build()
.expect("failed to build fallback tokio runtime for V8 delayed tasks")
});
Some(rt.enter())
}
pub fn create_runtime(dom: Dom, options: BrowserRuntimeOptions) -> JsRuntime {
create_runtime_with_signals(dom, options).0
}
pub fn create_runtime_with_signals(
dom: Dom,
options: BrowserRuntimeOptions,
) -> (JsRuntime, NavSignal) {
let mut state = DomState::new(dom);
state.stylesheets = options.stylesheets;
if let Some(storage) = options.storage {
state.storage = storage;
}
if let Some(url) = options.base_url {
state = state.with_base_url(url);
}
state.update_cached_rules();
let module_loader: Option<std::rc::Rc<dyn deno_core::ModuleLoader>> =
options.stealth_profile.as_ref().map(|p| {
std::rc::Rc::new(crate::js_runtime::module_loader::BrowserModuleLoader::new(
p.clone(),
)) as std::rc::Rc<dyn deno_core::ModuleLoader>
});
let fetch_state = match &options.stealth_profile {
Some(profile) => {
crate::js_runtime::extensions::fetch_ext::init_fetch_client(profile);
FetchState::with_profile(profile)
}
None => FetchState::new(None),
};
let stealth_state = StealthState::new_with_flags(
options.stealth_profile,
options.cross_origin_isolated,
options.is_secure_context,
);
let (heap_initial, heap_max) = heap_limits();
let create_params = deno_core::v8::CreateParams::default().heap_limits(heap_initial, heap_max);
let _tokio_guard = ensure_tokio_context();
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![
console_extension::init(),
crypto_extension::init(),
dom_extension::init(),
timer_extension::init(),
stealth_extension::init(),
fetch_extension::init(),
canvas_extension::init(),
layout_extension::init(),
websocket_extension::init(),
webgl_extension::init(),
sse_extension::init(),
input_extension::init(),
worker_extension::init(),
audio_extension::init(),
perf_extension::init(),
nav_extension::init(),
],
startup_snapshot: options.startup_snapshot,
create_params: Some(create_params),
shared_array_buffer_store: Some(SharedArrayBufferStore::default()),
module_loader,
..Default::default()
});
let nav_signal = NavSignal::new();
runtime.op_state().borrow_mut().put(state);
runtime.op_state().borrow_mut().put(TimerState::new());
runtime.op_state().borrow_mut().put(PerfState::new());
runtime
.op_state()
.borrow_mut()
.put(crate::js_runtime::extensions::input_ext::BehaviorRngState::from_env_or_random());
runtime.op_state().borrow_mut().put(nav_signal.clone());
runtime.op_state().borrow_mut().put(stealth_state);
runtime.op_state().borrow_mut().put(fetch_state);
runtime.op_state().borrow_mut().put(CanvasState::new());
runtime.op_state().borrow_mut().put(WebSocketState::new());
runtime.op_state().borrow_mut().put(WebGLState::new());
runtime.op_state().borrow_mut().put(SseState::new());
runtime
.op_state()
.borrow_mut()
.put(crate::js_runtime::extensions::worker_ext::WorkerOwnership::default());
let orig_fp_tostring: Option<deno_core::v8::Global<deno_core::v8::Function>> = {
let __ctx = runtime.main_context();
v8::scope_with_context!(scope, runtime.v8_isolate(), __ctx);
crate::js_runtime::native_fns::capture_original_fp_tostring(scope)
};
{
let mut realm_store = crate::js_runtime::native_fns::IframeRealmStore::new();
if let Some(ref orig) = orig_fp_tostring {
let __ctx = runtime.main_context();
v8::scope_with_context!(scope, runtime.v8_isolate(), __ctx);
let local = v8::Local::new(scope, orig);
realm_store.orig_fp_tostring = Some(v8::Global::new(scope, local));
}
runtime.op_state().borrow_mut().put(realm_store);
}
if options.startup_snapshot.is_none() {
const BOOTSTRAP_JS: &str = concat!(
include_str!("js/console_bootstrap.js"),
"\n",
include_str!("js/stealth_bootstrap.js"),
"\n",
include_str!("js/interfaces_bootstrap.js"),
"\n",
include_str!("js/shared_apis_bootstrap.js"),
"\n",
include_str!("js/instances_bootstrap.js"),
"\n",
include_str!("js/fetch_bootstrap.js"),
"\n",
include_str!("js/timer_bootstrap.js"),
"\n",
include_str!("js/dom_bootstrap.js"),
"\n",
include_str!("js/event_bootstrap.js"),
"\n",
include_str!("js/canvas_bootstrap.js"),
"\n",
include_str!("js/window_bootstrap.js"),
"\n",
include_str!("js/streams_bootstrap.js"),
"\n",
include_str!("js/structured_clone.js"),
);
runtime
.execute_script("<anonymous>", BOOTSTRAP_JS)
.expect("bootstrap failed");
}
runtime
.execute_script("<anonymous>", include_str!("js/cleanup_bootstrap.js"))
.expect("cleanup failed");
let native_tag_sym: Option<v8::Global<v8::Symbol>> = {
let __ctx = runtime.main_context();
v8::scope_with_context!(scope, runtime.v8_isolate(), __ctx);
let src = v8::String::new(scope, "Symbol.for('__browser_oxide_native__')");
src.and_then(|s| {
let script = v8::Script::compile(scope, s, None)?;
let val = script.run(scope)?;
let sym = v8::Local::<v8::Symbol>::try_from(val).ok()?;
Some(v8::Global::new(scope, sym))
})
};
if let Some(ref sym) = native_tag_sym {
let sym_clone = {
let __ctx = runtime.main_context();
v8::scope_with_context!(scope, runtime.v8_isolate(), __ctx);
let local = v8::Local::new(scope, sym);
v8::Global::new(scope, local)
};
runtime
.op_state()
.borrow_mut()
.borrow_mut::<crate::js_runtime::native_fns::IframeRealmStore>()
.native_tag_sym = Some(sym_clone);
}
if let Some(ref orig) = orig_fp_tostring {
let __ctx = runtime.main_context();
v8::scope_with_context!(scope, runtime.v8_isolate(), __ctx);
crate::js_runtime::native_fns::install_native_fp_tostring(
scope,
orig,
native_tag_sym.as_ref(),
);
}
for code in options.init_scripts.iter() {
if let Err(e) = runtime.execute_script("<anonymous>", code.clone()) {
tracing::warn!(error = %e, "init script failed");
}
}
(runtime, nav_signal)
}
pub fn create_worker_runtime(
profile: Option<StealthProfile>,
is_secure_context: bool,
) -> JsRuntime {
let _tokio_guard = ensure_tokio_context();
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![
console_extension::init(),
crypto_extension::init(),
timer_extension::init(),
fetch_extension::init(),
worker_extension::init(),
canvas_extension::init(),
stealth_extension::init(),
perf_extension::init(),
],
..Default::default()
});
runtime.op_state().borrow_mut().put(TimerState::new());
runtime.op_state().borrow_mut().put(FetchState::new(None));
runtime.op_state().borrow_mut().put(CanvasState::new());
runtime.op_state().borrow_mut().put(PerfState::default());
let mut dom_state = DomState::new(crate::dom::Dom::new());
dom_state.stealth_profile = profile.clone();
runtime.op_state().borrow_mut().put(dom_state);
runtime
.op_state()
.borrow_mut()
.put(StealthState::new_with_flags(
profile,
false,
is_secure_context,
));
runtime
.execute_script("<anonymous>", include_str!("js/stealth_bootstrap.js"))
.expect("worker: stealth bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/console_bootstrap.js"))
.expect("worker: console bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/interfaces_bootstrap.js"))
.expect("worker: interfaces bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/shared_apis_bootstrap.js"))
.expect("worker: shared_apis bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/timer_bootstrap.js"))
.expect("worker: timer bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/fetch_bootstrap.js"))
.expect("worker: fetch bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/streams_bootstrap.js"))
.expect("worker: streams bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/event_bootstrap.js"))
.expect("worker: event bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/structured_clone.js"))
.expect("worker: structured_clone bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/worker_bootstrap.js"))
.expect("worker: worker bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/canvas_bootstrap.js"))
.expect("worker: canvas bootstrap failed");
runtime
.execute_script("<anonymous>", include_str!("js/cleanup_bootstrap.js"))
.expect("worker: cleanup bootstrap failed");
runtime
}