chaser_cf/core/
browser.rs1use crate::error::{ChaserError, ChaserResult};
4use crate::models::ProxyConfig;
5
6use chaser_oxide::cdp::browser_protocol::target::CreateTargetParams;
7use chaser_oxide::{Browser, BrowserConfig, ChaserPage};
8use futures::StreamExt;
9use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
10use std::sync::Arc;
11use tokio::sync::Semaphore;
12
13pub struct BrowserManager {
14 browser: Browser,
15 context_semaphore: Arc<Semaphore>,
16 active_contexts: Arc<AtomicUsize>,
17 max_contexts: usize,
18 healthy: Arc<AtomicBool>,
19}
20
21impl BrowserManager {
22 pub async fn new(config: &super::ChaserConfig) -> ChaserResult<Self> {
23 let mut builder = BrowserConfig::builder().viewport(None).args(vec![
24 "--disable-blink-features=AutomationControlled".to_string(),
25 "--disable-infobars".to_string(),
26 ]);
27
28 if let Some(ref path) = config.chrome_path {
29 builder = builder.chrome_executable(path.clone());
30 }
31
32 if !config.headless {
33 builder = builder.with_head();
34 } else {
35 builder = builder.new_headless_mode();
36 }
37
38 let browser_config = builder
39 .build()
40 .map_err(|e| ChaserError::InitFailed(e.to_string()))?;
41
42 let (browser, mut handler) = Browser::launch(browser_config)
43 .await
44 .map_err(|e| ChaserError::InitFailed(e.to_string()))?;
45
46 let healthy = Arc::new(AtomicBool::new(true));
47 let healthy_clone = healthy.clone();
48 tokio::spawn(async move {
49 loop {
50 match handler.next().await {
51 Some(_) => {}
52 None => {
53 healthy_clone.store(false, Ordering::SeqCst);
54 break;
55 }
56 }
57 }
58 });
59
60 Ok(Self {
61 browser,
62 context_semaphore: Arc::new(Semaphore::new(config.context_limit)),
63 active_contexts: Arc::new(AtomicUsize::new(0)),
64 max_contexts: config.context_limit,
65 healthy,
66 })
67 }
68
69 pub fn is_healthy(&self) -> bool {
70 self.healthy.load(Ordering::SeqCst)
71 }
72
73 pub fn active_contexts(&self) -> usize {
74 self.active_contexts.load(Ordering::SeqCst)
75 }
76
77 pub fn max_contexts(&self) -> usize {
78 self.max_contexts
79 }
80
81 pub async fn acquire_permit(&self) -> ChaserResult<ContextPermit> {
82 let permit = self
83 .context_semaphore
84 .clone()
85 .acquire_owned()
86 .await
87 .map_err(|_| ChaserError::ContextFailed("Semaphore closed".to_string()))?;
88
89 self.active_contexts.fetch_add(1, Ordering::SeqCst);
90
91 Ok(ContextPermit {
92 _permit: permit,
93 active_contexts: self.active_contexts.clone(),
94 })
95 }
96
97 pub fn try_acquire_permit(&self) -> Option<ContextPermit> {
98 let permit = self.context_semaphore.clone().try_acquire_owned().ok()?;
99 self.active_contexts.fetch_add(1, Ordering::SeqCst);
100 Some(ContextPermit {
101 _permit: permit,
102 active_contexts: self.active_contexts.clone(),
103 })
104 }
105
106 pub async fn create_context(
107 &self,
108 proxy: Option<&ProxyConfig>,
109 ) -> ChaserResult<Option<chaser_oxide::cdp::browser_protocol::browser::BrowserContextId>> {
110 match proxy {
111 Some(p) => {
112 let ctx_id = self
113 .browser
114 .create_incognito_context_with_proxy(p.to_url())
115 .await
116 .map_err(|e| ChaserError::ContextFailed(e.to_string()))?;
117 Ok(Some(ctx_id))
118 }
119 None => Ok(None),
120 }
121 }
122
123 pub async fn new_page(
126 &self,
127 ctx_id: Option<chaser_oxide::cdp::browser_protocol::browser::BrowserContextId>,
128 url: &str,
129 ) -> ChaserResult<(chaser_oxide::Page, ChaserPage)> {
130 let mut params = CreateTargetParams::new("about:blank");
131 if let Some(id) = ctx_id {
132 params.browser_context_id = Some(id);
133 }
134
135 let page = self
136 .browser
137 .new_page(params)
138 .await
139 .map_err(|e| ChaserError::PageFailed(e.to_string()))?;
140
141 let chaser = ChaserPage::new(page.clone());
142
143 chaser
147 .apply_native_profile()
148 .await
149 .map_err(|e| ChaserError::PageFailed(format!("apply_native_profile: {e}")))?;
150
151 if url != "about:blank" {
152 chaser
153 .goto(url)
154 .await
155 .map_err(|e| ChaserError::NavigationFailed(e.to_string()))?;
156 }
157
158 Ok((page, chaser))
159 }
160
161 pub async fn shutdown(self) {
162 self.healthy.store(false, Ordering::SeqCst);
163 }
164}
165
166pub struct ContextPermit {
167 _permit: tokio::sync::OwnedSemaphorePermit,
168 active_contexts: Arc<AtomicUsize>,
169}
170
171impl Drop for ContextPermit {
172 fn drop(&mut self) {
173 self.active_contexts.fetch_sub(1, Ordering::SeqCst);
174 }
175}