use crate::backend::{AnyPage, CookieData};
use crate::error::Result;
use crate::network::Request;
use crate::page::Page;
use crate::state::SessionKey;
use arc_swap::ArcSwap;
use rustc_hash::FxHashMap as HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, serde::Serialize)]
pub struct DialogEvent {
pub dialog_type: String,
pub message: String,
pub action: String,
}
pub struct BrowserContext {
pub pages: Vec<AnyPage>,
pub active_page_idx: usize,
pub ref_map: Arc<ArcSwap<HashMap<String, i64>>>,
pub console_log: Arc<RwLock<Vec<crate::console_message::ConsoleMessage>>>,
pub network_log: Arc<RwLock<Vec<Request>>>,
pub dialog_log: Arc<RwLock<Vec<DialogEvent>>>,
name: String,
pub cdp_context_id: Option<String>,
}
impl BrowserContext {
pub(crate) fn new(name: String) -> Self {
Self {
pages: Vec::new(),
active_page_idx: 0,
ref_map: Arc::new(ArcSwap::from_pointee(HashMap::default())),
console_log: Arc::new(RwLock::new(Vec::new())),
network_log: Arc::new(RwLock::new(Vec::new())),
dialog_log: Arc::new(RwLock::new(Vec::new())),
name,
cdp_context_id: None,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn active_page(&self) -> Option<&AnyPage> {
self.pages.get(self.active_page_idx)
}
pub async fn cookies(&self) -> Result<Vec<CookieData>> {
if let Some(page) = self.active_page() {
page.get_cookies().await
} else {
Ok(Vec::new())
}
}
pub async fn add_cookies(&self, cookies: Vec<CookieData>) -> Result<()> {
let page = self.active_page().ok_or(crate::error::FerriError::NotConnected)?;
for cookie in cookies {
page.set_cookie(cookie).await?;
}
Ok(())
}
pub async fn clear_cookies(&self) -> Result<()> {
if let Some(page) = self.active_page() {
page.clear_cookies().await?;
}
Ok(())
}
pub async fn delete_cookie(&self, name: &str, domain: Option<&str>) -> Result<()> {
let cookies = self.cookies().await?;
if let Some(page) = self.active_page() {
page.clear_cookies().await?;
for cookie in cookies {
let name_matches = cookie.name == name;
let domain_matches = domain.is_none_or(|d| cookie.domain == d);
if !(name_matches && domain_matches) {
page.set_cookie(cookie).await?;
}
}
}
Ok(())
}
pub async fn console_messages(
&self,
level: Option<&str>,
limit: usize,
) -> Vec<crate::console_message::ConsoleMessage> {
let msgs = self.console_log.read().await;
msgs
.iter()
.filter(|m| level.is_none_or(|l| l == "all" || m.type_str() == l))
.rev()
.take(limit)
.cloned()
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect()
}
pub async fn network_requests(&self, limit: usize) -> Vec<Request> {
let reqs = self.network_log.read().await;
reqs
.iter()
.rev()
.take(limit)
.cloned()
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect()
}
pub async fn dialog_messages(&self, limit: usize) -> Vec<DialogEvent> {
let msgs = self.dialog_log.read().await;
let start = msgs.len().saturating_sub(limit);
msgs[start..].to_vec()
}
}
use crate::state::BrowserState;
#[derive(Clone)]
pub struct ContextRef {
pub(crate) state: Arc<RwLock<BrowserState>>,
pub(crate) name: Arc<str>,
pub(crate) key: SessionKey,
default_timeout_ms: u64,
default_navigation_timeout_ms: u64,
events: crate::events::ContextEventEmitter,
}
impl ContextRef {
pub fn new(state: Arc<RwLock<BrowserState>>, name: String) -> Self {
let key = SessionKey::parse(&name);
let events = match state.try_read() {
Ok(s) => s.get_or_create_context_events(&key.to_composite()),
Err(_) => crate::events::ContextEventEmitter::new(),
};
Self {
state,
name: Arc::from(name),
key,
default_timeout_ms: 0,
default_navigation_timeout_ms: 0,
events,
}
}
#[must_use]
pub fn events(&self) -> &crate::events::ContextEventEmitter {
&self.events
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
pub async fn new_page(&self) -> Result<Arc<Page>> {
{
let mut state = self.state.write().await;
Box::pin(state.ensure_instance(&self.key.instance)).await?;
}
let ctx_opts = {
let state = self.state.read().await;
state.get_context_options(&self.key.to_composite())
};
let resolved_viewport = match &ctx_opts {
Some(opts) => match opts.viewport {
crate::options::ViewportOption::Null => None,
crate::options::ViewportOption::Default | crate::options::ViewportOption::Size { .. } => {
opts.resolved_viewport()
},
},
None => None,
};
let plan = {
let state = self.state.read().await;
state.page_open_plan(&self.key)?
};
let effective_viewport = if ctx_opts
.as_ref()
.is_some_and(|o| o.viewport != crate::options::ViewportOption::Default)
{
resolved_viewport
} else {
plan.viewport.clone()
};
let (any_page, browser_context_id) = if &*self.key.context == "default" {
(
Box::pin(plan.browser.new_page(
"about:blank",
plan.browser_context_id.as_deref(),
effective_viewport.as_ref(),
))
.await?,
None,
)
} else if let Some(existing_ctx_id) = plan.browser_context_id.clone() {
(
Box::pin(
plan
.browser
.new_page("about:blank", Some(&existing_ctx_id), effective_viewport.as_ref()),
)
.await?,
Some(existing_ctx_id),
)
} else {
let ctx_id = plan.browser.new_context(ctx_opts.as_ref()).await?;
let page = Box::pin(
plan
.browser
.new_page("about:blank", Some(&ctx_id), effective_viewport.as_ref()),
)
.await?;
(page, Some(ctx_id))
};
{
let mut state = self.state.write().await;
state.register_opened_page(&self.key, any_page.clone(), browser_context_id)?;
}
let page = Page::with_context(any_page, self.clone());
if let Some(opts) = ctx_opts.as_ref() {
apply_context_options(&page, opts).await?;
if let Some(ref storage) = opts.storage_state {
let should_hydrate = {
let state = self.state.read().await;
state.claim_storage_state_hydration(&self.key.to_composite())
};
if should_hydrate {
let state_value = match storage {
crate::options::StorageStateInput::Inline(v) => v.clone(),
crate::options::StorageStateInput::Path(p) => {
let text = std::fs::read_to_string(p)
.map_err(|e| crate::error::FerriError::Backend(format!("storageState: read {}: {e}", p.display())))?;
serde_json::from_str(&text).map_err(|e| {
crate::error::FerriError::Backend(format!("storageState: parse JSON from {}: {e}", p.display()))
})?
},
};
page.set_storage_state(&state_value).await?;
}
}
}
let record_opts = {
let state = self.state.read().await;
state.get_record_video(&self.key.to_composite())
};
if let Some(opts) = record_opts {
start_video_recording(&page, &opts);
}
Ok(page)
}
pub async fn pages(&self) -> Result<Vec<Arc<Page>>> {
let inner_pages = {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
ctx.pages.clone()
};
let mut pages = Vec::with_capacity(inner_pages.len());
for inner in inner_pages {
pages.push(Page::with_context(inner, self.clone()));
}
Ok(pages)
}
pub async fn cookies(&self) -> Result<Vec<CookieData>> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
ctx.cookies().await
}
pub async fn add_cookies(&self, cookies: Vec<CookieData>) -> Result<()> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
ctx.add_cookies(cookies).await
}
pub async fn clear_cookies(&self) -> Result<()> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
ctx.clear_cookies().await
}
pub async fn clear_cookies_filtered(&self, options: &crate::backend::ClearCookieOptions) -> Result<()> {
if options.name.is_none() && options.domain.is_none() && options.path.is_none() {
return self.clear_cookies().await;
}
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
let cookies = ctx.cookies().await?;
if let Some(page) = ctx.active_page() {
page.clear_cookies().await?;
for c in cookies {
let name_match = options.name.as_ref().is_none_or(|n| &c.name == n);
let domain_match = options.domain.as_ref().is_none_or(|d| &c.domain == d);
let path_match = options.path.as_ref().is_none_or(|p| &c.path == p);
if !(name_match && domain_match && path_match) {
page.set_cookie(c).await?;
}
}
}
Ok(())
}
pub async fn delete_cookie(&self, name: &str, domain: Option<&str>) -> Result<()> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
ctx.delete_cookie(name, domain).await
}
pub fn set_default_timeout(&mut self, ms: u64) {
self.default_timeout_ms = ms;
}
pub fn set_default_navigation_timeout(&mut self, ms: u64) {
self.default_navigation_timeout_ms = ms;
}
async fn mutate_options<F>(&self, f: F) -> Result<()>
where
F: FnOnce(&mut crate::options::BrowserContextOptions),
{
let composite = self.key.to_composite();
let updated = {
let state = self.state.read().await;
let mut opts = state.get_context_options(&composite).unwrap_or_default();
f(&mut opts);
state.set_context_options(&composite, opts.clone());
opts
};
let pages = Box::pin(self.pages()).await?;
for page in pages {
Box::pin(page.apply_context_options(&updated)).await?;
}
Ok(())
}
pub async fn grant_permissions(&self, permissions: &[String], _origin: Option<&str>) -> Result<()> {
let perms = permissions.to_vec();
self.mutate_options(|o| o.permissions = Some(perms)).await
}
pub async fn clear_permissions(&self) -> Result<()> {
let pages = self.pages().await?;
for page in &pages {
page.inner().reset_permissions().await?;
}
self.mutate_options(|o| o.permissions = None).await
}
pub async fn close(&self) -> Result<()> {
let mut state = self.state.write().await;
let persistent = state.persistent_context;
state.remove_context(&self.name).await;
if persistent {
state.shutdown().await;
}
Ok(())
}
#[must_use]
pub fn state(&self) -> &Arc<RwLock<BrowserState>> {
&self.state
}
pub async fn set_record_video(&self, opts: crate::options::RecordVideoOptions) -> Result<()> {
let composite = self.key.to_composite();
let state = self.state.read().await;
state.set_record_video(&composite, opts.clone());
let mut bag = state.get_context_options(&composite).unwrap_or_default();
bag.record_video = Some(opts);
state.set_context_options(&composite, bag);
Ok(())
}
pub fn on(&self, event_name: &str, callback: crate::events::ContextEventCallback) -> crate::events::ListenerId {
self.events.on(event_name, callback)
}
pub fn once(&self, event_name: &str, callback: crate::events::ContextEventCallback) -> crate::events::ListenerId {
self.events.once(event_name, callback)
}
pub fn off(&self, id: crate::events::ListenerId) {
self.events.off(id);
}
pub async fn wait_for_event(&self, event_name: &str, timeout_ms: u64) -> Result<crate::events::ContextEvent> {
self.events.wait_for_event(event_name, timeout_ms).await
}
pub async fn add_init_script(
&self,
script: crate::options::InitScriptSource,
arg: Option<serde_json::Value>,
) -> Result<Vec<String>> {
let source = crate::options::evaluation_script(script, arg.as_ref())?;
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
let mut ids = Vec::new();
for page in &ctx.pages {
ids.push(page.add_init_script(&source).await?);
}
Ok(ids)
}
pub async fn set_geolocation(&self, lat: f64, lng: f64, accuracy: f64) -> Result<()> {
self
.mutate_options(|o| {
o.geolocation = Some(crate::options::Geolocation {
latitude: lat,
longitude: lng,
accuracy,
});
})
.await
}
pub async fn set_extra_http_headers(&self, headers: &rustc_hash::FxHashMap<String, String>) -> Result<()> {
let headers = headers.clone();
self.mutate_options(|o| o.extra_http_headers = Some(headers)).await
}
pub async fn set_offline(&self, offline: bool) -> Result<()> {
self.mutate_options(|o| o.offline = Some(offline)).await
}
pub async fn route(
&self,
matcher: crate::url_matcher::UrlMatcher,
handler: crate::route::RouteHandler,
) -> Result<()> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
for page in &ctx.pages {
page.route(matcher.clone(), handler.clone()).await?;
}
Ok(())
}
pub async fn unroute(&self, matcher: &crate::url_matcher::UrlMatcher) -> Result<()> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
for page in &ctx.pages {
page.unroute(matcher).await?;
}
Ok(())
}
}
async fn apply_context_options(page: &Arc<Page>, opts: &crate::options::BrowserContextOptions) -> Result<()> {
Box::pin(page.apply_context_options(opts)).await
}
fn start_video_recording(page: &Arc<Page>, opts: &crate::options::RecordVideoOptions) {
static VIDEO_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let (video, sink) = crate::video::Video::new();
page.attach_video(Arc::new(video));
let size = opts.size.unwrap_or_default();
let width = size.width & !1;
let height = size.height & !1;
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_millis());
let id = VIDEO_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let filename = format!("{millis}-{id}.{}", crate::video::video_extension());
let output_path = opts.dir.join(filename);
if let Err(e) = std::fs::create_dir_all(&opts.dir) {
sink.finish_err(crate::FerriError::backend(format!(
"failed to create recordVideo.dir {}: {e}",
opts.dir.display()
)));
return;
}
let page_for_task = page.clone();
tokio::spawn(async move {
let handle = match crate::video::start_recording(&page_for_task, output_path.clone(), width, height, 90).await {
Ok(h) => h,
Err(e) => {
sink.finish_err(crate::FerriError::backend(format!("start_recording: {e}")));
return;
},
};
while !page_for_task.is_closed() {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
match handle.stop(&page_for_task).await {
Ok(path) => sink.finish_ok(path),
Err(e) => sink.finish_err(crate::FerriError::backend(format!("stop recording: {e}"))),
}
});
}