1mod download_handling;
8mod event_listener;
9mod page_handlers;
10mod types;
11
12use std::collections::HashMap;
13use std::future::Future;
14use std::path::PathBuf;
15use std::sync::Arc;
16use std::time::Duration;
17
18use tokio::sync::{Mutex, RwLock, oneshot};
19use tracing::{debug, warn};
20use viewpoint_cdp::CdpConnection;
21
22use super::console::ConsoleMessage;
23use super::dialog::Dialog;
24use super::download::Download;
25use super::file_chooser::FileChooser;
26use super::frame::Frame;
27use super::page_error::PageError as PageErrorInfo;
28use crate::error::PageError;
29
30pub use types::{
31 ConsoleHandler, DialogHandler, DownloadHandler, FileChooserHandler, FrameAttachedHandler,
32 FrameDetachedHandler, FrameNavigatedHandler, PageErrorHandler,
33};
34
35use download_handling::DownloadTracker;
36
37pub struct PageEventManager {
39 connection: Arc<CdpConnection>,
41 session_id: String,
43 dialog_handler: Arc<RwLock<Option<DialogHandler>>>,
45 download_handler: Arc<RwLock<Option<DownloadHandler>>>,
47 file_chooser_handler: Arc<RwLock<Option<FileChooserHandler>>>,
49 console_handler: Arc<RwLock<Option<ConsoleHandler>>>,
51 pageerror_handler: Arc<RwLock<Option<PageErrorHandler>>>,
53 frameattached_handler: Arc<RwLock<Option<FrameAttachedHandler>>>,
55 framenavigated_handler: Arc<RwLock<Option<FrameNavigatedHandler>>>,
57 framedetached_handler: Arc<RwLock<Option<FrameDetachedHandler>>>,
59 downloads: Arc<Mutex<HashMap<String, DownloadTracker>>>,
61 download_dir: PathBuf,
63 file_chooser_intercepted: Arc<RwLock<bool>>,
65 wait_for_dialog_tx: Arc<Mutex<Option<oneshot::Sender<Dialog>>>>,
67 wait_for_download_tx: Arc<Mutex<Option<oneshot::Sender<Download>>>>,
69 wait_for_file_chooser_tx: Arc<Mutex<Option<oneshot::Sender<FileChooser>>>>,
71 wait_for_console_tx: Arc<Mutex<Option<oneshot::Sender<ConsoleMessage>>>>,
73 wait_for_pageerror_tx: Arc<Mutex<Option<oneshot::Sender<PageErrorInfo>>>>,
75}
76
77impl std::fmt::Debug for PageEventManager {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("PageEventManager")
81 .field("session_id", &self.session_id)
82 .field("download_dir", &self.download_dir)
83 .finish_non_exhaustive()
84 }
85}
86
87impl PageEventManager {
88 pub fn new(connection: Arc<CdpConnection>, session_id: String) -> Self {
90 let download_dir = std::env::temp_dir().join("viewpoint-downloads");
91 let manager = Self {
92 connection: connection.clone(),
93 session_id: session_id.clone(),
94 dialog_handler: Arc::new(RwLock::new(None)),
95 download_handler: Arc::new(RwLock::new(None)),
96 file_chooser_handler: Arc::new(RwLock::new(None)),
97 console_handler: Arc::new(RwLock::new(None)),
98 pageerror_handler: Arc::new(RwLock::new(None)),
99 frameattached_handler: Arc::new(RwLock::new(None)),
100 framenavigated_handler: Arc::new(RwLock::new(None)),
101 framedetached_handler: Arc::new(RwLock::new(None)),
102 downloads: Arc::new(Mutex::new(HashMap::new())),
103 download_dir,
104 file_chooser_intercepted: Arc::new(RwLock::new(false)),
105 wait_for_dialog_tx: Arc::new(Mutex::new(None)),
106 wait_for_download_tx: Arc::new(Mutex::new(None)),
107 wait_for_file_chooser_tx: Arc::new(Mutex::new(None)),
108 wait_for_console_tx: Arc::new(Mutex::new(None)),
109 wait_for_pageerror_tx: Arc::new(Mutex::new(None)),
110 };
111
112 manager.start_event_listener();
114
115 manager
116 }
117
118 fn start_event_listener(&self) {
120 event_listener::start_event_listener(
121 self.connection.clone(),
122 self.session_id.clone(),
123 self.console_handler.clone(),
124 self.pageerror_handler.clone(),
125 self.dialog_handler.clone(),
126 self.frameattached_handler.clone(),
127 self.framenavigated_handler.clone(),
128 self.framedetached_handler.clone(),
129 self.wait_for_console_tx.clone(),
130 self.wait_for_pageerror_tx.clone(),
131 self.wait_for_dialog_tx.clone(),
132 self.download_handler.clone(),
134 self.downloads.clone(),
135 self.wait_for_download_tx.clone(),
136 );
137 }
138
139 pub async fn set_dialog_handler<F, Fut>(&self, handler: F)
141 where
142 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
143 Fut: Future<Output = Result<(), PageError>> + Send + 'static,
144 {
145 let mut dialog_handler = self.dialog_handler.write().await;
146 *dialog_handler = Some(Box::new(move |dialog| Box::pin(handler(dialog))));
147 }
148
149 pub async fn remove_dialog_handler(&self) {
151 let mut dialog_handler = self.dialog_handler.write().await;
152 *dialog_handler = None;
153 }
154
155 pub async fn set_download_handler<F, Fut>(&self, handler: F)
157 where
158 F: Fn(Download) -> Fut + Send + Sync + 'static,
159 Fut: Future<Output = ()> + Send + 'static,
160 {
161 let mut download_handler = self.download_handler.write().await;
162 *download_handler = Some(Box::new(move |download| Box::pin(handler(download))));
163 }
164
165 pub async fn set_file_chooser_handler<F, Fut>(&self, handler: F)
167 where
168 F: Fn(FileChooser) -> Fut + Send + Sync + 'static,
169 Fut: Future<Output = ()> + Send + 'static,
170 {
171 let mut file_chooser_handler = self.file_chooser_handler.write().await;
172 *file_chooser_handler = Some(Box::new(move |chooser| Box::pin(handler(chooser))));
173 }
174
175 pub async fn set_console_handler<F, Fut>(&self, handler: F)
177 where
178 F: Fn(ConsoleMessage) -> Fut + Send + Sync + 'static,
179 Fut: Future<Output = ()> + Send + 'static,
180 {
181 let mut console_handler = self.console_handler.write().await;
182 *console_handler = Some(Box::new(move |message| Box::pin(handler(message))));
183 }
184
185 pub async fn remove_console_handler(&self) {
187 let mut console_handler = self.console_handler.write().await;
188 *console_handler = None;
189 }
190
191 pub async fn set_pageerror_handler<F, Fut>(&self, handler: F)
193 where
194 F: Fn(PageErrorInfo) -> Fut + Send + Sync + 'static,
195 Fut: Future<Output = ()> + Send + 'static,
196 {
197 let mut pageerror_handler = self.pageerror_handler.write().await;
198 *pageerror_handler = Some(Box::new(move |error| Box::pin(handler(error))));
199 }
200
201 pub async fn remove_pageerror_handler(&self) {
203 let mut pageerror_handler = self.pageerror_handler.write().await;
204 *pageerror_handler = None;
205 }
206
207 pub async fn set_frameattached_handler<F, Fut>(&self, handler: F)
209 where
210 F: Fn(Frame) -> Fut + Send + Sync + 'static,
211 Fut: Future<Output = ()> + Send + 'static,
212 {
213 let mut frameattached_handler = self.frameattached_handler.write().await;
214 *frameattached_handler = Some(Box::new(move |frame| Box::pin(handler(frame))));
215 }
216
217 pub async fn remove_frameattached_handler(&self) {
219 let mut frameattached_handler = self.frameattached_handler.write().await;
220 *frameattached_handler = None;
221 }
222
223 pub async fn set_framenavigated_handler<F, Fut>(&self, handler: F)
225 where
226 F: Fn(Frame) -> Fut + Send + Sync + 'static,
227 Fut: Future<Output = ()> + Send + 'static,
228 {
229 let mut framenavigated_handler = self.framenavigated_handler.write().await;
230 *framenavigated_handler = Some(Box::new(move |frame| Box::pin(handler(frame))));
231 }
232
233 pub async fn remove_framenavigated_handler(&self) {
235 let mut framenavigated_handler = self.framenavigated_handler.write().await;
236 *framenavigated_handler = None;
237 }
238
239 pub async fn set_framedetached_handler<F, Fut>(&self, handler: F)
241 where
242 F: Fn(Frame) -> Fut + Send + Sync + 'static,
243 Fut: Future<Output = ()> + Send + 'static,
244 {
245 let mut framedetached_handler = self.framedetached_handler.write().await;
246 *framedetached_handler = Some(Box::new(move |frame| Box::pin(handler(frame))));
247 }
248
249 pub async fn remove_framedetached_handler(&self) {
251 let mut framedetached_handler = self.framedetached_handler.write().await;
252 *framedetached_handler = None;
253 }
254
255 pub async fn handle_frame_attached(&self, frame: Frame) {
257 let handler = self.frameattached_handler.read().await;
258 if let Some(ref h) = *handler {
259 h(frame).await;
260 }
261 }
262
263 pub async fn handle_frame_navigated(&self, frame: Frame) {
265 let handler = self.framenavigated_handler.read().await;
266 if let Some(ref h) = *handler {
267 h(frame).await;
268 }
269 }
270
271 pub async fn handle_frame_detached(&self, frame: Frame) {
273 let handler = self.framedetached_handler.read().await;
274 if let Some(ref h) = *handler {
275 h(frame).await;
276 }
277 }
278
279 pub async fn set_intercept_file_chooser(&self, enabled: bool) -> Result<(), PageError> {
281 *self.file_chooser_intercepted.write().await = enabled;
282
283 self.connection
284 .send_command::<_, serde_json::Value>(
285 "Page.setInterceptFileChooserDialog",
286 Some(serde_json::json!({ "enabled": enabled })),
287 Some(&self.session_id),
288 )
289 .await?;
290
291 Ok(())
292 }
293
294 pub async fn set_download_behavior(&self, allow: bool) -> Result<(), PageError> {
296 if allow {
298 tokio::fs::create_dir_all(&self.download_dir)
299 .await
300 .map_err(|e| {
301 PageError::EvaluationFailed(format!("Failed to create download directory: {e}"))
302 })?;
303 }
304
305 let behavior = if allow { "allow" } else { "deny" };
306
307 self.connection
308 .send_command::<_, serde_json::Value>(
309 "Browser.setDownloadBehavior",
310 Some(serde_json::json!({
311 "behavior": behavior,
312 "downloadPath": self.download_dir.to_string_lossy(),
313 "eventsEnabled": true,
314 })),
315 Some(&self.session_id),
316 )
317 .await?;
318
319 Ok(())
320 }
321
322 pub async fn handle_dialog_event(
324 &self,
325 dialog_type: viewpoint_cdp::protocol::DialogType,
326 message: String,
327 default_prompt: Option<String>,
328 ) {
329 let dialog = Dialog::new(
330 self.connection.clone(),
331 self.session_id.clone(),
332 dialog_type,
333 message,
334 default_prompt,
335 );
336
337 {
339 let mut waiter = self.wait_for_dialog_tx.lock().await;
340 if let Some(tx) = waiter.take() {
341 let _ = tx.send(dialog);
342 return;
343 }
344 }
345
346 let handler = self.dialog_handler.read().await;
348 if let Some(ref h) = *handler {
349 if let Err(e) = h(dialog).await {
350 warn!("Dialog handler failed: {}", e);
351 }
352 } else {
353 debug!("Auto-dismissing dialog (no handler registered)");
355 if let Err(e) = dialog.dismiss().await {
356 warn!("Failed to auto-dismiss dialog: {}", e);
357 }
358 }
359 }
360
361 pub async fn handle_download_begin(
363 &self,
364 guid: String,
365 suggested_filename: String,
366 url: String,
367 ) {
368 download_handling::handle_download_begin(
369 &self.connection,
370 &self.session_id,
371 &self.downloads,
372 &self.download_handler,
373 &self.wait_for_download_tx,
374 guid,
375 suggested_filename,
376 url,
377 )
378 .await;
379 }
380
381 pub async fn handle_download_progress(&self, guid: String, state: &str) {
383 download_handling::handle_download_progress(&self.downloads, guid, state).await;
384 }
385
386 pub async fn handle_file_chooser_event(
388 &self,
389 frame_id: String,
390 mode: viewpoint_cdp::protocol::page::FileChooserMode,
391 backend_node_id: Option<i32>,
392 ) {
393 download_handling::handle_file_chooser_event(
394 &self.connection,
395 &self.session_id,
396 &self.file_chooser_handler,
397 &self.wait_for_file_chooser_tx,
398 frame_id,
399 mode,
400 backend_node_id,
401 )
402 .await;
403 }
404
405 pub async fn wait_for_dialog(&self, timeout: Duration) -> Result<Dialog, PageError> {
407 let (tx, rx) = oneshot::channel();
408 {
409 let mut waiter = self.wait_for_dialog_tx.lock().await;
410 *waiter = Some(tx);
411 }
412
413 tokio::time::timeout(timeout, rx)
414 .await
415 .map_err(|_| PageError::EvaluationFailed("Timeout waiting for dialog".to_string()))?
416 .map_err(|_| PageError::EvaluationFailed("Dialog wait cancelled".to_string()))
417 }
418
419 pub async fn wait_for_download(&self, timeout: Duration) -> Result<Download, PageError> {
421 download_handling::wait_for_download(&self.wait_for_download_tx, timeout).await
422 }
423
424 pub async fn wait_for_file_chooser(&self, timeout: Duration) -> Result<FileChooser, PageError> {
426 download_handling::wait_for_file_chooser(&self.wait_for_file_chooser_tx, timeout).await
427 }
428
429 pub async fn wait_for_console(&self, timeout: Duration) -> Result<ConsoleMessage, PageError> {
431 let (tx, rx) = oneshot::channel();
432 {
433 let mut waiter = self.wait_for_console_tx.lock().await;
434 *waiter = Some(tx);
435 }
436
437 tokio::time::timeout(timeout, rx)
438 .await
439 .map_err(|_| {
440 PageError::EvaluationFailed("Timeout waiting for console message".to_string())
441 })?
442 .map_err(|_| PageError::EvaluationFailed("Console wait cancelled".to_string()))
443 }
444
445 pub async fn wait_for_pageerror(&self, timeout: Duration) -> Result<PageErrorInfo, PageError> {
447 let (tx, rx) = oneshot::channel();
448 {
449 let mut waiter = self.wait_for_pageerror_tx.lock().await;
450 *waiter = Some(tx);
451 }
452
453 tokio::time::timeout(timeout, rx)
454 .await
455 .map_err(|_| PageError::EvaluationFailed("Timeout waiting for page error".to_string()))?
456 .map_err(|_| PageError::EvaluationFailed("Page error wait cancelled".to_string()))
457 }
458}