refactor: drop dead code and refresh comments after the per-connection move
- Delete the now-unused CdpContext::new_full (its only caller switched to new_with_shared_context); have _new_inner build the shared BrowserContext and delegate, so the struct's field list lives in one place. - Extract drive_module_eval, shared verbatim by load_module and load_inline_module. - Update comments that still described the deleted process-wide V8 lock and the single-LocalSet model to the per-connection reality, drop the dangling reference to the removed obscura-js/src/v8_lock.rs, and soften the ISOLATE_CREATE_LOCK note now that the main-thread warmup is the real guard against the InitializeBuiltinJSDispatchTable segfault. Refs #430.
This commit is contained in:
1 parent
9d2f9f23ee
commit
1f5bffc4de
4 files changed
+69
-140
No files matched your search
@@ -100,21 +100,6 @@ impl CdpContext {
|
||||
Self::_new_inner(proxy, stealth, user_agent, None, allow_file_access, false)
|
||||
}
|
||||
|
||||
/// Kitchen-sink constructor that also threads `allow_private_network`
|
||||
/// (issue #33). Older `new_with_*` builders stay as-is.
|
||||
pub fn new_full(
|
||||
proxy: Option<String>,
|
||||
stealth: bool,
|
||||
user_agent: Option<String>,
|
||||
storage_dir: Option<std::path::PathBuf>,
|
||||
allow_file_access: bool,
|
||||
allow_private_network: bool,
|
||||
) -> Self {
|
||||
Self::_new_inner(
|
||||
proxy, stealth, user_agent, storage_dir, allow_file_access, allow_private_network,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a context that reuses an already-constructed, shared
|
||||
/// [`BrowserContext`]. The thread-per-connection server (issue #430 /
|
||||
/// "Option 2") gives each connection its own `CdpContext` on its own OS
|
||||
@@ -122,6 +107,11 @@ impl CdpContext {
|
||||
/// connections must still share one cookie jar and one HTTP client. Passing
|
||||
/// the same `Arc<BrowserContext>` to every connection's context does that.
|
||||
pub fn new_with_shared_context(default_context: Arc<BrowserContext>) -> Self {
|
||||
// Pre-seed with the default-frame execution context ids that
|
||||
// `Runtime.enable` (1) and post-navigation re-emission (2) advertise via
|
||||
// Runtime.executionContextCreated. Anything else has to be registered
|
||||
// explicitly (Page.createIsolatedWorld), otherwise
|
||||
// Runtime.{evaluate,callFunctionOn} should reject it per CDP spec.
|
||||
let mut valid_context_ids = HashSet::new();
|
||||
valid_context_ids.insert(1);
|
||||
valid_context_ids.insert(2);
|
||||
@@ -160,32 +150,7 @@ impl CdpContext {
|
||||
allow_private_network,
|
||||
);
|
||||
ctx.allow_file_access = allow_file_access;
|
||||
let default_context = Arc::new(ctx);
|
||||
// Pre-seed with the default-frame execution context ids that
|
||||
// `Runtime.enable` (1) and post-navigation re-emission (2) advertise
|
||||
// via Runtime.executionContextCreated. Anything else has to be
|
||||
// registered explicitly (Page.createIsolatedWorld), otherwise
|
||||
// Runtime.{evaluate,callFunctionOn} should reject it per CDP spec.
|
||||
let mut valid_context_ids = HashSet::new();
|
||||
valid_context_ids.insert(1);
|
||||
valid_context_ids.insert(2);
|
||||
|
||||
CdpContext {
|
||||
pages: Vec::new(),
|
||||
sessions: HashMap::new(),
|
||||
pending_events: Vec::new(),
|
||||
default_context,
|
||||
page_counter: 0,
|
||||
preload_scripts: Vec::new(),
|
||||
preload_counter: 0,
|
||||
fetch_intercept: FetchInterceptState::new(),
|
||||
intercept_tx: None,
|
||||
isolated_worlds: Vec::new(),
|
||||
valid_context_ids,
|
||||
next_isolated_context_id: 100,
|
||||
io_streams: crate::domains::io::IoStreamStore::default(),
|
||||
v8_lock: Arc::new(tokio::sync::Mutex::new(())),
|
||||
}
|
||||
Self::new_with_shared_context(Arc::new(ctx))
|
||||
}
|
||||
|
||||
/// Claim the next isolated-world execution context id and register it as
|
||||
@@ -250,7 +215,7 @@ impl CdpContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a CDP method can be served WITHOUT acquiring the global V8 lock.
|
||||
/// Whether a CDP method can be served WITHOUT acquiring the per-connection V8 lock.
|
||||
///
|
||||
/// Methods listed here were audited to confirm they do not transitively
|
||||
/// call into a `JsRuntime`. They either don't touch any `Page` at all, or
|
||||
@@ -295,41 +260,31 @@ fn is_v8_free_method(method: &str) -> bool {
|
||||
pub async fn dispatch(req: &CdpRequest, ctx: &mut CdpContext) -> CdpResponse {
|
||||
// headless_chrome (and older Puppeteer) wrap every CDP call inside
|
||||
// Target.sendMessageToTarget. Unwrap and recurse BEFORE acquiring the
|
||||
// V8 lock — the recursive dispatch will acquire it for the inner call,
|
||||
// and tokio Mutex is not reentrant.
|
||||
// per-connection V8 lock — the recursive dispatch will acquire it for the
|
||||
// inner call, and tokio Mutex is not reentrant.
|
||||
if req.method == "Target.sendMessageToTarget" {
|
||||
return dispatch_send_message_to_target(req, ctx).await;
|
||||
}
|
||||
|
||||
// Issue #19: V8 fatal abort under concurrent CDP work.
|
||||
// Issue #430: keep this connection's V8 work serialized on its own thread.
|
||||
//
|
||||
// Every CDP handler below may end up calling into a per-Page `JsRuntime`
|
||||
// (each owning its own V8 Isolate). All of them run on a single OS
|
||||
// thread (current_thread tokio + LocalSet). When two pages' V8-touching
|
||||
// futures interleave across an `.await` (which `process_with_interception`
|
||||
// can trigger by handling new CDP messages while a navigation task is in
|
||||
// flight on `spawn_local`), V8 trips the
|
||||
// `heap->isolate() == Isolate::TryGetCurrent()` invariant and aborts the
|
||||
// process via `V8_Fatal` — no Rust panic, just `abort(3)`.
|
||||
// Every CDP handler below may call into a per-Page `JsRuntime` (each owning
|
||||
// its own V8 Isolate). With the thread-per-connection server each connection
|
||||
// runs on its own OS thread, so isolates never collide across connections.
|
||||
// Within one connection, though, a nav task spawned by
|
||||
// `process_with_interception` runs while this processor keeps pumping other
|
||||
// CDP messages, so two of this connection's pages could still interleave V8
|
||||
// work on this one thread and trip
|
||||
// `heap->isolate() == Isolate::TryGetCurrent()`. The per-connection lock
|
||||
// (`ctx.v8_lock`) keeps each handler contiguous: V8 fully exits one Isolate
|
||||
// before the next of this connection's pages is allowed in. It is
|
||||
// per-connection, not process-wide, so other connections run in parallel.
|
||||
//
|
||||
// Holding the process-wide V8 lock around the entire dispatch keeps each
|
||||
// handler contiguous on the thread: V8 fully exits one Isolate before
|
||||
// the next page is allowed in. This converts the abort into latency.
|
||||
// It overshoots — non-V8 handlers (Browser.*, Storage.*, Emulation.*)
|
||||
// also serialize — but those are cheap and the safety win dominates.
|
||||
//
|
||||
// The properly concurrent fix is to pin each `JsRuntime` to its own OS
|
||||
// thread and message-pass; that's tracked as the larger #19 follow-up.
|
||||
//
|
||||
// Optimization: methods that demonstrably never touch V8 bypass the
|
||||
// lock. During Puppeteer's newPage() setup ~8 such calls (Page.enable,
|
||||
// Runtime.enable, Page.getFrameTree, Page.addScriptToEvaluateOnNewDocument,
|
||||
// Page.createIsolatedWorld, etc.) used to serialize behind any
|
||||
// in-flight V8 work on a sibling page. Bypassing keeps the setup chain
|
||||
// running while a separate nav executes. Each listed method was audited
|
||||
// to confirm it never reaches `JsRuntime::execute_script` or DOM
|
||||
// mutation that re-enters V8; `get_session_page_mut` (which can
|
||||
// trigger `suspend_js`/`resume_js`) is NOT in the list.
|
||||
// Optimization: methods that demonstrably never touch V8 bypass the lock
|
||||
// (Puppeteer's newPage() setup issues ~8 such calls). Each listed method was
|
||||
// audited to confirm it never reaches `JsRuntime::execute_script` or DOM
|
||||
// mutation that re-enters V8; `get_session_page_mut` (which can trigger
|
||||
// `suspend_js`/`resume_js`) is NOT in the list.
|
||||
let _v8_guard = if is_v8_free_method(&req.method) {
|
||||
None
|
||||
} else {
|
||||
@@ -341,8 +296,8 @@ pub async fn dispatch(req: &CdpRequest, ctx: &mut CdpContext) -> CdpResponse {
|
||||
|
||||
// Per-command V8 watchdog. The lock above keeps each handler contiguous on
|
||||
// the thread, but it does not bound how long a handler runs: a hung page (a
|
||||
// runaway Runtime.evaluate, a synchronous DOM op) would hold the
|
||||
// process-wide V8 lock and wedge every other session forever. The one-shot
|
||||
// runaway Runtime.evaluate, a synchronous DOM op) would hold this
|
||||
// connection's V8 lock and wedge its other sessions forever. The one-shot
|
||||
// CLI uses a process-level hard deadline for this; the long-running server
|
||||
// cannot force-exit, so we terminate just the offending isolate instead.
|
||||
// OBSCURA_CDP_COMMAND_TIMEOUT_MS tunes the bound (0 disables); the default
|
||||
|
||||
@@ -178,7 +178,7 @@ pub async fn start_with_full_serve_options(
|
||||
// reuses it (one cookie jar, one HTTP client) while running on its own OS
|
||||
// thread, so each page's V8 isolate is confined to a single thread and the
|
||||
// #430 cross-page abort cannot happen (V8's TryGetCurrent check is
|
||||
// per-thread). This is the "Option 2" fix named in obscura-js/src/v8_lock.rs.
|
||||
// per-thread). This is the thread-per-runtime fix for #430.
|
||||
let mut bctx = obscura_browser::BrowserContext::with_storage_and_network(
|
||||
"default".to_string(),
|
||||
proxy,
|
||||
@@ -460,8 +460,8 @@ async fn cdp_processor(
|
||||
loop {
|
||||
// Drain any deferred messages from the previous interception window
|
||||
// before pulling new ones off the wire. Each is processed with no
|
||||
// nav-task spawn_local in flight, so dispatch's v8_lock can claim
|
||||
// the only Isolate cleanly.
|
||||
// nav-task spawn_local in flight, so this connection's only entered
|
||||
// Isolate is the one dispatch is about to touch.
|
||||
let msg = if let Some(d) = deferred.pop_front() {
|
||||
d
|
||||
} else {
|
||||
@@ -710,7 +710,7 @@ async fn process_with_interception(
|
||||
// `heap->isolate() == Isolate::TryGetCurrent()` invariant and aborts
|
||||
// the process via `V8_Fatal`.
|
||||
//
|
||||
// The `obscura_js::v8_lock` mutex doesn't save us here: it's a
|
||||
// This connection's `ctx.v8_lock` doesn't save us here: it's a
|
||||
// `tokio::sync::Mutex` that is released around `.await`s inside V8
|
||||
// ops, so it doesn't actually keep the V8 enter/exit pair contiguous
|
||||
// on the thread.
|
||||
|
||||
Reference in new issue
Block a user