use crate::js_runtime::extensions::stealth_ext::StealthState;
use crate::js_runtime::state::DomState;
use deno_core::op2;
use deno_core::OpState;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, Mutex, OnceLock};
use tokio::sync::Notify;
struct BlobEntry {
data: Vec<u8>,
content_type: String,
}
struct BlobRegistry {
blobs: HashMap<String, BlobEntry>,
}
fn blob_registry() -> &'static Mutex<BlobRegistry> {
static INST: OnceLock<Mutex<BlobRegistry>> = OnceLock::new();
INST.get_or_init(|| {
Mutex::new(BlobRegistry {
blobs: HashMap::new(),
})
})
}
#[op2(fast)]
pub fn op_blob_register(
#[string] url: String,
#[buffer] data: &[u8],
#[string] content_type: String,
) {
tracing::debug!(
url = %url,
bytes = data.len(),
content_type = %content_type,
"op_blob_register"
);
let mut reg = blob_registry().lock().unwrap_or_else(|e| e.into_inner());
reg.blobs.insert(
url,
BlobEntry {
data: data.to_vec(),
content_type,
},
);
}
#[op2]
#[string]
pub fn op_blob_fetch_text(#[string] url: String) -> String {
let reg = blob_registry().lock().unwrap_or_else(|e| e.into_inner());
match reg.blobs.get(&url) {
Some(entry) => String::from_utf8_lossy(&entry.data).to_string(),
None => String::new(),
}
}
#[derive(serde::Serialize)]
pub struct JsBlobResponse {
pub bytes: Vec<u8>,
pub content_type: String,
pub found: bool,
}
#[op2]
#[serde]
pub fn op_blob_fetch_bytes(#[string] url: String) -> JsBlobResponse {
let reg = blob_registry().lock().unwrap_or_else(|e| e.into_inner());
match reg.blobs.get(&url) {
Some(entry) => JsBlobResponse {
bytes: entry.data.clone(),
content_type: entry.content_type.clone(),
found: true,
},
None => JsBlobResponse {
bytes: Vec::new(),
content_type: String::new(),
found: false,
},
}
}
#[op2(fast)]
pub fn op_blob_revoke(#[string] url: String) {
let mut reg = blob_registry().lock().unwrap_or_else(|e| e.into_inner());
reg.blobs.remove(&url);
}
#[op2]
#[string]
pub fn op_worker_sync_fetch(#[string] url: String) -> String {
let client = match crate::js_runtime::extensions::fetch_ext::fetch_client() {
Some(c) => c,
None => match crate::net::HttpClient::new(&crate::stealth::chrome_148_linux()) {
Ok(c) => c,
Err(_) => return String::new(),
},
};
let (tx, rx) = std::sync::mpsc::channel::<String>();
std::thread::spawn(move || {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(_) => {
let _ = tx.send(String::new());
return;
}
};
let body = rt.block_on(async move {
match client.get(&url).await {
Ok(resp) if resp.ok() => resp.text(),
_ => String::new(),
}
});
let _ = tx.send(body);
});
rx.recv_timeout(std::time::Duration::from_secs(30))
.unwrap_or_default()
}
struct WorkerSlot {
to_worker: Sender<String>,
from_worker: Receiver<String>,
terminate: Arc<AtomicBool>,
notify_parent: Arc<Notify>,
}
fn worker_registry() -> &'static Mutex<HashMap<u32, WorkerSlot>> {
static INST: OnceLock<Mutex<HashMap<u32, WorkerSlot>>> = OnceLock::new();
INST.get_or_init(|| Mutex::new(HashMap::new()))
}
static NEXT_WORKER_ID: AtomicU32 = AtomicU32::new(1);
struct WorkerSelf {
to_parent: Sender<String>,
from_parent: Receiver<String>,
notify_parent: Arc<Notify>,
url: String,
}
thread_local! {
static WORKER_SELF: RefCell<Option<WorkerSelf>> = const { RefCell::new(None) };
}
#[op2(fast)]
#[smi]
pub fn op_worker_spawn(
op_state: &mut OpState,
#[string] script: String,
#[string] _name: String,
is_module: bool,
#[string] url: String,
) -> i32 {
let state = op_state.borrow::<DomState>();
let stealth = op_state.borrow::<StealthState>();
let owned = op_state.borrow::<WorkerOwnership>();
let profile = stealth
.profile
.clone()
.or_else(|| state.stealth_profile.clone());
let is_secure_context = stealth.is_secure_context;
let parent_fetch_client = crate::js_runtime::extensions::fetch_ext::fetch_client();
let (to_worker_tx, to_worker_rx) = std::sync::mpsc::channel::<String>();
let (to_parent_tx, to_parent_rx) = std::sync::mpsc::channel::<String>();
let terminate = Arc::new(AtomicBool::new(false));
let notify_parent = Arc::new(Notify::new());
let worker_id = NEXT_WORKER_ID.fetch_add(1, Ordering::Relaxed);
tracing::debug!(
worker_id,
is_module,
is_secure_context,
url = %url,
script_len = script.len(),
"op_worker_spawn"
);
{
let mut reg = worker_registry().lock().unwrap_or_else(|e| e.into_inner());
reg.insert(
worker_id,
WorkerSlot {
to_worker: to_worker_tx,
from_worker: to_parent_rx,
terminate: terminate.clone(),
notify_parent: notify_parent.clone(),
},
);
}
owned.spawned_ids.borrow_mut().push(worker_id);
let thread_result = std::thread::Builder::new()
.name(format!("worker-{worker_id}"))
.stack_size(64 * 1024 * 1024)
.spawn(move || {
WORKER_SELF.with(|w| {
*w.borrow_mut() = Some(WorkerSelf {
to_parent: to_parent_tx,
from_parent: to_worker_rx,
notify_parent: notify_parent.clone(),
url,
});
});
let seed_client = parent_fetch_client
.or_else(|| profile.as_ref().and_then(|p| crate::net::HttpClient::new(p).ok()));
if let Some(c) = seed_client {
crate::js_runtime::extensions::fetch_ext::set_fetch_client(c);
}
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
tracing::error!(worker_id = worker_id, error = %e, "worker tokio build error");
return;
}
};
let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move {
let mut runtime =
crate::js_runtime::runtime::create_worker_runtime(profile, is_secure_context);
if is_module {
let specifier = deno_core::ModuleSpecifier::parse(&format!(
"worker-oxide://{worker_id}/main.mjs"
))
.expect("worker-oxide URL parses");
match runtime
.load_main_es_module_from_code(&specifier, script)
.await
{
Ok(mod_id) => {
let eval_fut = runtime.mod_evaluate(mod_id);
if let Err(e) = eval_fut.await {
tracing::warn!(
worker_id = worker_id, error = %e, "worker module eval error"
);
}
}
Err(e) => {
tracing::error!(worker_id = worker_id, error = %e, "worker module load error");
}
}
} else if let Err(e) = runtime.execute_script("<anonymous>", script) {
tracing::warn!(worker_id = worker_id, error = %e, "worker script error");
}
while !terminate.load(Ordering::Acquire) {
let fut = Box::pin(
runtime.run_event_loop(deno_core::PollEventLoopOptions::default()),
);
let tick =
tokio::time::timeout(std::time::Duration::from_millis(25), fut).await;
match tick {
Ok(Ok(())) => {
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
Ok(Err(e)) => {
tracing::warn!(worker_id = worker_id, error = %e, "worker event loop error");
break;
}
Err(_) => {
continue;
}
}
}
WORKER_SELF.with(|w| *w.borrow_mut() = None);
});
});
if let Err(e) = thread_result {
tracing::error!(worker_id = worker_id, error = %e, "worker thread spawn failed");
worker_registry()
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&worker_id);
return 0;
}
worker_id as i32
}
#[op2(fast)]
pub fn op_worker_post_to_worker(#[smi] worker_id: i32, #[string] data: String) {
let reg = worker_registry().lock().unwrap_or_else(|e| e.into_inner());
if let Some(slot) = reg.get(&(worker_id as u32)) {
let _ = slot.to_worker.send(data);
}
}
#[op2]
#[string]
pub fn op_worker_poll_from_worker(#[smi] worker_id: i32) -> String {
let reg = worker_registry().lock().unwrap_or_else(|e| e.into_inner());
if let Some(slot) = reg.get(&(worker_id as u32)) {
match slot.from_worker.try_recv() {
Ok(msg) => return msg,
Err(_) => return String::new(),
}
}
String::new()
}
#[op2(fast)]
pub fn op_worker_terminate(#[smi] worker_id: i32) {
terminate_worker_inner(worker_id as u32);
}
pub fn terminate_worker_inner(worker_id: u32) {
let mut reg = worker_registry().lock().unwrap_or_else(|e| e.into_inner());
if let Some(slot) = reg.get(&worker_id) {
slot.terminate.store(true, Ordering::Release);
slot.notify_parent.notify_waiters();
}
reg.remove(&worker_id);
}
pub fn drain_owned_workers(state: &mut OpState) {
let ids: Vec<u32> = state
.try_borrow::<WorkerOwnership>()
.map(|o| std::mem::take(&mut *o.spawned_ids.borrow_mut()))
.unwrap_or_default();
for id in ids {
terminate_worker_inner(id);
}
}
#[derive(Default)]
pub struct WorkerOwnership {
pub spawned_ids: RefCell<Vec<u32>>,
}
#[op2(async(lazy), fast)]
#[string]
pub async fn op_worker_await_message(#[smi] worker_id: i32) -> String {
let id = worker_id as u32;
let (notify, terminate, fast_msg) = {
let reg = worker_registry().lock().unwrap_or_else(|e| e.into_inner());
match reg.get(&id) {
Some(slot) => {
let already = slot.from_worker.try_recv().ok();
(slot.notify_parent.clone(), slot.terminate.clone(), already)
}
None => return String::new(), }
};
if let Some(msg) = fast_msg {
return msg;
}
loop {
if terminate.load(Ordering::Acquire) {
return String::new();
}
notify.notified().await;
let reg = worker_registry().lock().unwrap_or_else(|e| e.into_inner());
match reg.get(&id) {
Some(slot) => {
if let Ok(msg) = slot.from_worker.try_recv() {
return msg;
}
}
None => return String::new(),
}
}
}
#[op2(fast)]
pub fn op_worker_self_post(#[string] data: String) {
WORKER_SELF.with(|w| {
if let Some(s) = w.borrow().as_ref() {
let _ = s.to_parent.send(data);
s.notify_parent.notify_one();
}
});
}
#[op2]
#[string]
pub fn op_worker_self_recv() -> String {
WORKER_SELF.with(|w| {
if let Some(s) = w.borrow().as_ref() {
s.from_parent.try_recv().unwrap_or_default()
} else {
String::new()
}
})
}
#[op2]
#[string]
pub fn op_worker_self_url() -> String {
WORKER_SELF.with(|w| {
if let Some(s) = w.borrow().as_ref() {
s.url.clone()
} else {
String::new()
}
})
}
deno_core::extension!(
worker_extension,
ops = [
op_blob_register,
op_blob_fetch_text,
op_blob_fetch_bytes,
op_blob_revoke,
op_worker_sync_fetch,
op_worker_spawn,
op_worker_post_to_worker,
op_worker_poll_from_worker,
op_worker_await_message,
op_worker_terminate,
op_worker_self_post,
op_worker_self_url,
op_worker_self_recv,
],
);