1use tokio::sync::oneshot;
2
3use crate::error::NightFuryError;
4use crate::session::BrowserSession;
5use crate::worker::WorkerState;
6
7#[non_exhaustive]
13pub enum ContextCmd {
14 NewContext {
16 reply: oneshot::Sender<Result<String, String>>,
17 },
18 NewContextTab {
20 context_id: String,
21 url: Option<String>,
22 reply: oneshot::Sender<Result<usize, String>>,
23 },
24 CloseContext {
26 context_id: String,
27 reply: oneshot::Sender<Result<String, String>>,
28 },
29}
30
31impl ContextCmd {
36 pub(crate) async fn dispatch(self, state: &mut WorkerState) {
37 match self {
38 ContextCmd::NewContext { reply } => handle_new_context(state, reply).await,
39 ContextCmd::NewContextTab {
40 context_id,
41 url,
42 reply,
43 } => handle_new_context_tab(state, context_id, url, reply).await,
44 ContextCmd::CloseContext { context_id, reply } => {
45 handle_close_context(state, context_id, reply).await
46 }
47 }
48 }
49}
50
51async fn handle_new_context(
56 state: &mut WorkerState,
57 reply: oneshot::Sender<Result<String, String>>,
58) {
59 let result: Result<String, String> = async {
60 let ctx_id = state
61 .browser
62 .create_browser_context(
63 chromiumoxide_cdp::cdp::browser_protocol::target::CreateBrowserContextParams::default(),
64 )
65 .await
66 .map_err(|e| format!("CreateBrowserContext failed: {e}"))?;
67 Ok(ctx_id.inner().to_string())
68 }
69 .await;
70 let _ = reply.send(result);
71}
72
73async fn handle_new_context_tab(
74 state: &mut WorkerState,
75 context_id: String,
76 url: Option<String>,
77 reply: oneshot::Sender<Result<usize, String>>,
78) {
79 let result: Result<usize, String> = async {
80 use chaser_oxide::ChaserPage;
81 use chromiumoxide_cdp::cdp::browser_protocol::browser::BrowserContextId;
82 use chromiumoxide_cdp::cdp::browser_protocol::target::CreateTargetParams;
83
84 let target_url = url.as_deref().unwrap_or("about:blank");
85 let ctx_id_str = context_id.clone();
86 let mut params = CreateTargetParams::new(target_url);
87 params.browser_context_id = Some(BrowserContextId::from(context_id));
88
89 let page = state
90 .browser
91 .new_page(params)
92 .await
93 .map_err(|e| format!("new_page in context failed: {e}"))?;
94 let chaser = ChaserPage::new(page);
95
96 if let Some(ref profile) = state.stealth_profile {
97 chaser
98 .apply_profile(profile)
99 .await
100 .map_err(|e| format!("apply_profile failed: {e}"))?;
101 }
102
103 let idx = state.tabs.len();
104 state.tabs.push(crate::worker::TabState {
105 page: chaser,
106 snapshot_refs: Vec::new(),
107 active_frame: None,
108 browser_context_id: Some(ctx_id_str),
109 });
110 state.active_tab = idx;
111
112 crate::domains::tabs::spawn_page_listeners(
113 &state.tabs[idx].page,
114 std::sync::Arc::clone(&state.dialog_config),
115 std::sync::Arc::clone(&state.last_dialog),
116 std::sync::Arc::clone(&state.console_enabled),
117 std::sync::Arc::clone(&state.console_log),
118 )
119 .await;
120
121 Ok(idx)
122 }
123 .await;
124 let _ = reply.send(result);
125}
126
127async fn handle_close_context(
128 state: &mut WorkerState,
129 context_id: String,
130 reply: oneshot::Sender<Result<String, String>>,
131) {
132 let result: Result<String, String> = async {
133 use chromiumoxide_cdp::cdp::browser_protocol::browser::BrowserContextId;
134
135 state
136 .browser
137 .dispose_browser_context(BrowserContextId::from(context_id.clone()))
138 .await
139 .map_err(|e| format!("DisposeBrowserContext failed: {e}"))?;
140
141 let mut i = state.tabs.len();
144 while i > 0 {
145 i -= 1;
146 if state.tabs[i].browser_context_id.as_deref() == Some(&context_id) {
147 state.tabs.remove(i);
148 if state.active_tab >= state.tabs.len() && !state.tabs.is_empty() {
150 state.active_tab = state.tabs.len() - 1;
151 }
152 }
153 }
154
155 Ok(format!("Disposed browser context {context_id}"))
156 }
157 .await;
158 let _ = reply.send(result);
159}
160
161#[derive(Clone, Debug)]
171pub struct BrowserContext {
172 session: BrowserSession,
173 context_id: String,
174}
175
176impl BrowserContext {
177 pub(crate) fn new(session: BrowserSession, context_id: String) -> Self {
178 Self {
179 session,
180 context_id,
181 }
182 }
183
184 pub fn id(&self) -> &str {
186 &self.context_id
187 }
188
189 pub async fn new_tab(&self, url: Option<&str>) -> Result<usize, NightFuryError> {
192 send_cmd!(
193 self.session,
194 |tx| crate::cmd::BrowserCmd::Context(ContextCmd::NewContextTab {
195 context_id: self.context_id.clone(),
196 url: url.map(String::from),
197 reply: tx,
198 }),
199 NightFuryError::OperationFailed
200 )
201 }
202
203 pub async fn close(self) -> Result<String, NightFuryError> {
205 send_cmd!(
206 self.session,
207 |tx| crate::cmd::BrowserCmd::Context(ContextCmd::CloseContext {
208 context_id: self.context_id.clone(),
209 reply: tx,
210 }),
211 NightFuryError::OperationFailed
212 )
213 }
214}
215
216impl BrowserSession {
221 pub async fn new_context(&self) -> Result<BrowserContext, NightFuryError> {
225 let context_id: String = send_cmd!(
226 self,
227 |tx| crate::cmd::BrowserCmd::Context(ContextCmd::NewContext { reply: tx }),
228 NightFuryError::OperationFailed
229 )?;
230 Ok(BrowserContext::new(self.clone(), context_id))
231 }
232}
233
234#[cfg(test)]
239mod tests {
240 use super::*;
241 use tokio::sync::mpsc;
242
243 #[test]
244 fn browser_context_clone_debug() {
245 let (tx, _rx) = mpsc::channel::<crate::cmd::BrowserCmd>(1);
246 let session = BrowserSession::from_sender(tx);
247 let ctx = BrowserContext::new(session, "test-ctx-id".to_string());
248 let cloned = ctx.clone();
249 assert_eq!(cloned.id(), "test-ctx-id");
250 let dbg = format!("{:?}", cloned);
252 assert!(dbg.contains("test-ctx-id"));
253 }
254
255 #[test]
256 fn browser_context_id() {
257 let (tx, _rx) = mpsc::channel::<crate::cmd::BrowserCmd>(1);
258 let session = BrowserSession::from_sender(tx);
259 let ctx = BrowserContext::new(session, "abc-123".to_string());
260 assert_eq!(ctx.id(), "abc-123");
261 }
262
263 #[tokio::test]
264 async fn new_context_worker_dead() {
265 let (tx, rx) = mpsc::channel::<crate::cmd::BrowserCmd>(1);
266 drop(rx);
267 let session = BrowserSession::from_sender(tx);
268 let err = session.new_context().await.unwrap_err();
269 assert!(
270 matches!(err, NightFuryError::WorkerDead),
271 "expected WorkerDead, got {err:?}"
272 );
273 }
274
275 #[tokio::test]
276 async fn new_context_tab_worker_dead() {
277 let (tx, rx) = mpsc::channel::<crate::cmd::BrowserCmd>(1);
278 drop(rx);
279 let session = BrowserSession::from_sender(tx);
280 let ctx = BrowserContext::new(session, "dead-ctx".to_string());
281 let err = ctx.new_tab(Some("https://example.com")).await.unwrap_err();
282 assert!(
283 matches!(err, NightFuryError::WorkerDead),
284 "expected WorkerDead, got {err:?}"
285 );
286 }
287
288 #[tokio::test]
289 async fn close_context_worker_dead() {
290 let (tx, rx) = mpsc::channel::<crate::cmd::BrowserCmd>(1);
291 drop(rx);
292 let session = BrowserSession::from_sender(tx);
293 let ctx = BrowserContext::new(session, "dead-ctx".to_string());
294 let err = ctx.close().await.unwrap_err();
295 assert!(
296 matches!(err, NightFuryError::WorkerDead),
297 "expected WorkerDead, got {err:?}"
298 );
299 }
300}