1pub use browser::Browser;
14pub use element::Element;
15#[cfg(feature = "atexit")]
16pub use exit_hook::ExitHook;
17pub use tab::Tab;
18
19#[derive(Debug, Clone)]
41pub struct Viewport {
42 pub width: u32,
44 pub height: u32,
46 pub device_scale_factor: f64,
49 pub is_mobile: bool,
51 pub has_touch: bool,
53 pub is_landscape: bool,
55}
56
57impl Default for Viewport {
58 fn default() -> Self {
59 Self {
60 width: 800,
61 height: 600,
62 device_scale_factor: 1.0,
63 is_mobile: false,
64 has_touch: false,
65 is_landscape: false,
66 }
67 }
68}
69
70impl Viewport {
71 pub fn new(width: u32, height: u32) -> Self {
73 Self {
74 width,
75 height,
76 ..Default::default()
77 }
78 }
79
80 pub fn builder() -> ViewportBuilder {
82 ViewportBuilder::default()
83 }
84
85 pub fn with_device_scale_factor(mut self, factor: f64) -> Self {
92 self.device_scale_factor = factor;
93 self
94 }
95
96 pub fn with_mobile(mut self, is_mobile: bool) -> Self {
98 self.is_mobile = is_mobile;
99 self
100 }
101
102 pub fn with_touch(mut self, has_touch: bool) -> Self {
104 self.has_touch = has_touch;
105 self
106 }
107
108 pub fn with_landscape(mut self, is_landscape: bool) -> Self {
110 self.is_landscape = is_landscape;
111 self
112 }
113}
114
115#[derive(Debug, Clone, Default)]
117pub struct ViewportBuilder {
118 width: Option<u32>,
119 height: Option<u32>,
120 device_scale_factor: Option<f64>,
121 is_mobile: Option<bool>,
122 has_touch: Option<bool>,
123 is_landscape: Option<bool>,
124}
125
126impl ViewportBuilder {
127 pub fn width(mut self, width: u32) -> Self {
129 self.width = Some(width);
130 self
131 }
132
133 pub fn height(mut self, height: u32) -> Self {
135 self.height = Some(height);
136 self
137 }
138
139 pub fn device_scale_factor(mut self, factor: f64) -> Self {
141 self.device_scale_factor = Some(factor);
142 self
143 }
144
145 pub fn is_mobile(mut self, mobile: bool) -> Self {
147 self.is_mobile = Some(mobile);
148 self
149 }
150
151 pub fn has_touch(mut self, touch: bool) -> Self {
153 self.has_touch = Some(touch);
154 self
155 }
156
157 pub fn is_landscape(mut self, landscape: bool) -> Self {
159 self.is_landscape = Some(landscape);
160 self
161 }
162
163 pub fn build(self) -> Viewport {
165 let default = Viewport::default();
166 Viewport {
167 width: self.width.unwrap_or(default.width),
168 height: self.height.unwrap_or(default.height),
169 device_scale_factor: self
170 .device_scale_factor
171 .unwrap_or(default.device_scale_factor),
172 is_mobile: self.is_mobile.unwrap_or(default.is_mobile),
173 has_touch: self.has_touch.unwrap_or(default.has_touch),
174 is_landscape: self.is_landscape.unwrap_or(default.is_landscape),
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
181pub enum ImageFormat {
182 #[default]
184 Jpeg,
185 Png,
187 WebP,
189}
190
191impl ImageFormat {
192 pub fn as_str(&self) -> &'static str {
194 match self {
195 ImageFormat::Jpeg => "jpeg",
196 ImageFormat::Png => "png",
197 ImageFormat::WebP => "webp",
198 }
199 }
200}
201
202#[derive(Debug, Clone, Default)]
217pub struct CaptureOptions {
218 pub(crate) format: ImageFormat,
220 pub(crate) quality: Option<u8>,
222 pub(crate) viewport: Option<Viewport>,
224 pub(crate) full_page: bool,
226 pub(crate) omit_background: bool,
228 pub(crate) clip: Option<ClipRegion>,
230}
231
232#[derive(Debug, Clone, Copy)]
234pub struct ClipRegion {
235 pub x: f64,
237 pub y: f64,
239 pub width: f64,
241 pub height: f64,
243 pub scale: f64,
245}
246
247impl ClipRegion {
248 pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
250 Self {
251 x,
252 y,
253 width,
254 height,
255 scale: 1.0,
256 }
257 }
258
259 pub fn with_scale(mut self, scale: f64) -> Self {
261 self.scale = scale;
262 self
263 }
264}
265
266impl CaptureOptions {
267 pub fn new() -> Self {
269 Self::default()
270 }
271
272 pub fn with_format(mut self, format: ImageFormat) -> Self {
274 self.format = format;
275 self
276 }
277
278 pub fn with_quality(mut self, quality: u8) -> Self {
280 self.quality = Some(quality.min(100));
281 self
282 }
283
284 pub fn with_viewport(mut self, viewport: Viewport) -> Self {
289 self.viewport = Some(viewport);
290 self
291 }
292
293 pub fn with_full_page(mut self, full_page: bool) -> Self {
295 self.full_page = full_page;
296 self
297 }
298
299 pub fn with_omit_background(mut self, omit: bool) -> Self {
301 self.omit_background = omit;
302 self
303 }
304
305 pub fn with_clip(mut self, clip: ClipRegion) -> Self {
307 self.clip = Some(clip);
308 self
309 }
310
311 pub fn raw_png() -> Self {
313 Self::new().with_format(ImageFormat::Png)
314 }
315
316 pub fn high_quality_jpeg() -> Self {
318 Self::new().with_format(ImageFormat::Jpeg).with_quality(95)
319 }
320
321 pub fn hidpi() -> Self {
323 Self::new().with_viewport(Viewport::default().with_device_scale_factor(2.0))
324 }
325
326 pub fn ultra_hidpi() -> Self {
328 Self::new().with_viewport(Viewport::default().with_device_scale_factor(3.0))
329 }
330
331 #[deprecated(since = "0.2.0", note = "Use `with_format()` instead")]
334 pub fn with_raw_png(mut self, raw: bool) -> Self {
335 self.format = if raw {
336 ImageFormat::Png
337 } else {
338 ImageFormat::Jpeg
339 };
340 self
341 }
342}
343
344mod transport {
348 use anyhow::{Result, anyhow};
349 use futures_util::stream::{SplitSink, SplitStream};
350 use futures_util::{SinkExt, StreamExt};
351 use serde::{Deserialize, Serialize};
352 use serde_json::{Value, json};
353 use std::collections::HashMap;
354 use std::sync::atomic::{AtomicUsize, Ordering};
355 use std::time::Duration;
356 use tokio::net::TcpStream;
357 use tokio::sync::{mpsc, oneshot};
358 use tokio::time;
359 use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async, tungstenite::Message};
360
361 pub(crate) static GLOBAL_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
363
364 pub(crate) fn next_id() -> usize {
366 GLOBAL_ID_COUNTER.fetch_add(1, Ordering::SeqCst) + 1
367 }
368
369 #[derive(Debug)]
371 pub(crate) enum TransportMessage {
372 Request(Value, oneshot::Sender<Result<TransportResponse>>),
374 ListenTargetMessage(u64, oneshot::Sender<Result<TransportResponse>>),
376 WaitForEvent(String, String, oneshot::Sender<()>),
378 Shutdown,
380 }
381
382 #[derive(Debug)]
384 pub(crate) enum TransportResponse {
385 Response(Response),
387 Target(TargetMessage),
389 }
390
391 #[derive(Debug, Serialize, Deserialize)]
393 pub(crate) struct Response {
394 pub(crate) id: u64,
396 pub(crate) result: Value,
398 }
399
400 #[derive(Debug, Serialize, Deserialize)]
402 pub(crate) struct TargetMessage {
403 pub(crate) params: Value,
405 }
406
407 struct TransportActor {
409 pending_requests: HashMap<u64, oneshot::Sender<Result<TransportResponse>>>,
411 event_listeners: HashMap<(String, String), Vec<oneshot::Sender<()>>>,
413 ws_sink: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
415 command_rx: mpsc::Receiver<TransportMessage>,
417 }
418
419 impl TransportActor {
420 async fn run(
422 mut self,
423 mut ws_stream: SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
424 ) {
425 loop {
426 tokio::select! {
427 Some(msg) = ws_stream.next() => {
428 match msg {
429 Ok(Message::Text(text)) => {
430 if let Ok(response) = serde_json::from_str::<Response>(&text) {
432 if let Some(sender) = self.pending_requests.remove(&response.id) {
433 let _ = sender.send(Ok(TransportResponse::Response(response)));
434 }
435 }
436 else if let Ok(target_msg) = serde_json::from_str::<TargetMessage>(&text)
438 && let Some(inner_str) = target_msg.params.get("message").and_then(|v| v.as_str())
439 && let Ok(inner_json) = serde_json::from_str::<Value>(inner_str) {
440
441 if let Some(id) = inner_json.get("id").and_then(|i| i.as_u64()) {
443 if let Some(sender) = self.pending_requests.remove(&id) {
444 let _ = sender.send(Ok(TransportResponse::Target(target_msg)));
445 }
446 }
447 else if let Some(method) = inner_json.get("method").and_then(|s| s.as_str())
449 && let Some(session_id) = target_msg.params.get("sessionId").and_then(|s| s.as_str()) {
450 let key = (session_id.to_string(), method.to_string());
451 if let Some(senders) = self.event_listeners.remove(&key) {
452 for tx in senders {
453 let _ = tx.send(());
454 }
455 }
456 }
457 }
458 }
459 Err(_) => break,
460 _ => {}
461 }
462 }
463 Some(msg) = self.command_rx.recv() => {
464 match msg {
465 TransportMessage::Request(cmd, tx) => {
466 if let Some(id) = cmd["id"].as_u64()
467 && let Ok(text) = serde_json::to_string(&cmd) {
468 if self.ws_sink.send(Message::Text(text)).await.is_ok() {
469 self.pending_requests.insert(id, tx);
470 } else {
471 let _ = tx.send(Err(anyhow!("WebSocket send failed")));
472 }
473 }
474 },
475 TransportMessage::ListenTargetMessage(id, tx) => {
476 self.pending_requests.insert(id, tx);
477 },
478 TransportMessage::WaitForEvent(session_id, method, tx) => {
480 self.event_listeners.entry((session_id, method)).or_default().push(tx);
481 },
482 TransportMessage::Shutdown => {
483 let _ = self.ws_sink.send(Message::Text(json!({
484 "id": next_id(),
485 "method": "Browser.close",
486 "params": {}
487 }).to_string())).await;
488 let _ = self.ws_sink.close().await;
489 break;
490 }
491 }
492 }
493 else => break,
494 }
495 }
496 }
497 }
498
499 #[derive(Debug)]
501 pub(crate) struct Transport {
502 tx: mpsc::Sender<TransportMessage>,
504 }
505
506 impl Transport {
507 pub(crate) async fn new(ws_url: &str) -> Result<Self> {
509 let (ws_stream, _) = connect_async(ws_url).await?;
510 let (ws_sink, ws_stream) = ws_stream.split();
511 let (tx, rx) = mpsc::channel(100);
512
513 tokio::spawn(async move {
514 let actor = TransportActor {
515 pending_requests: HashMap::new(),
516 event_listeners: HashMap::new(),
517 ws_sink,
518 command_rx: rx,
519 };
520 actor.run(ws_stream).await;
521 });
522
523 Ok(Self { tx })
524 }
525
526 pub(crate) async fn send(&self, command: Value) -> Result<TransportResponse> {
528 let (tx, rx) = oneshot::channel();
529 self.tx
530 .send(TransportMessage::Request(command, tx))
531 .await
532 .map_err(|_| anyhow!("Transport actor dropped"))?;
533 time::timeout(Duration::from_secs(30), rx)
534 .await
535 .map_err(|_| anyhow!("Timeout waiting for response"))?
536 .map_err(|_| anyhow!("Response channel closed"))?
537 }
538
539 pub(crate) async fn get_target_msg(&self, msg_id: usize) -> Result<TransportResponse> {
541 let (tx, rx) = oneshot::channel();
542 self.tx
543 .send(TransportMessage::ListenTargetMessage(msg_id as u64, tx))
544 .await
545 .map_err(|_| anyhow!("Transport actor dropped"))?;
546 time::timeout(Duration::from_secs(30), rx)
547 .await
548 .map_err(|_| anyhow!("Timeout waiting for target message"))?
549 .map_err(|_| anyhow!("Response channel closed"))?
550 }
551
552 pub(crate) async fn wait_for_event(&self, session_id: &str, method: &str) -> Result<()> {
554 let (tx, rx) = oneshot::channel();
555 self.tx
556 .send(TransportMessage::WaitForEvent(
557 session_id.to_string(),
558 method.to_string(),
559 tx,
560 ))
561 .await
562 .map_err(|_| anyhow!("Transport actor dropped"))?;
563
564 time::timeout(Duration::from_secs(30), rx)
565 .await
566 .map_err(|_| anyhow!("Timeout waiting for event {}", method))?
567 .map_err(|_| anyhow!("Event channel closed"))?;
568 Ok(())
569 }
570
571 pub(crate) async fn shutdown(&self) {
573 let _ = self.tx.send(TransportMessage::Shutdown).await;
574 }
575 }
576}
577
578mod utils {
582 use crate::transport::{TargetMessage, Transport, TransportResponse, next_id};
583 use anyhow::{Result, anyhow};
584 use serde_json::{Value, json};
585 use std::sync::Arc;
586
587 pub(crate) fn serde_msg(msg: &TargetMessage) -> Result<Value> {
589 let str_msg = msg.params["message"]
590 .as_str()
591 .ok_or_else(|| anyhow!("Invalid message format"))?;
592 Ok(serde_json::from_str(str_msg)?)
593 }
594
595 pub(crate) async fn send_and_get_msg(
597 transport: Arc<Transport>,
598 msg_id: usize,
599 session_id: &str,
600 msg: String,
601 ) -> Result<TargetMessage> {
602 let send_fut = transport.send(json!({
603 "id": next_id(),
604 "method": "Target.sendMessageToTarget",
605 "params": { "sessionId": session_id, "message": msg }
606 }));
607 let recv_fut = transport.get_target_msg(msg_id);
608
609 let (_, target_msg) = futures_util::try_join!(send_fut, recv_fut)?;
610
611 match target_msg {
612 TransportResponse::Target(res) => Ok(res),
613 other => Err(anyhow!("Unexpected response: {:?}", other)),
614 }
615 }
616}
617
618mod element {
622 use crate::tab::Tab;
623 use crate::transport::next_id;
624 use crate::utils::{self, send_and_get_msg};
625 use crate::{CaptureOptions, ImageFormat};
626 use anyhow::{Context, Result};
627 use serde_json::json;
628
629 pub struct Element<'a> {
631 parent: &'a Tab,
632 backend_node_id: u64,
633 }
634
635 impl<'a> Element<'a> {
636 pub(crate) async fn new(parent: &'a Tab, node_id: u64) -> Result<Self> {
638 let msg_id = next_id();
639 let msg = json!({
640 "id": msg_id,
641 "method": "DOM.describeNode",
642 "params": { "nodeId": node_id, "depth": 100 }
643 })
644 .to_string();
645
646 let res =
647 send_and_get_msg(parent.transport.clone(), msg_id, &parent.session_id, msg).await?;
648 let data = utils::serde_msg(&res)?;
649 let backend_node_id = data["result"]["node"]["backendNodeId"]
650 .as_u64()
651 .context("Missing backendNodeId")?;
652
653 Ok(Self {
654 parent,
655 backend_node_id,
656 })
657 }
658
659 pub async fn screenshot(&self) -> Result<String> {
661 self.screenshot_with_options(CaptureOptions::new().with_quality(90))
662 .await
663 }
664
665 pub async fn raw_screenshot(&self) -> Result<String> {
667 self.screenshot_with_options(CaptureOptions::raw_png())
668 .await
669 }
670
671 pub async fn screenshot_with_options(&self, opts: CaptureOptions) -> Result<String> {
682 if let Some(ref viewport) = opts.viewport {
684 self.parent.set_viewport(viewport).await?;
685 }
686
687 let msg_id = next_id();
689 let msg_box = json!({
690 "id": msg_id,
691 "method": "DOM.getBoxModel",
692 "params": { "backendNodeId": self.backend_node_id }
693 })
694 .to_string();
695
696 let res_box = send_and_get_msg(
697 self.parent.transport.clone(),
698 msg_id,
699 &self.parent.session_id,
700 msg_box,
701 )
702 .await?;
703 let data_box = utils::serde_msg(&res_box)?;
704 let border = &data_box["result"]["model"]["border"];
705
706 let (x, y, w, h) = (
707 border[0].as_f64().unwrap_or(0.0),
708 border[1].as_f64().unwrap_or(0.0),
709 (border[2].as_f64().unwrap_or(0.0) - border[0].as_f64().unwrap_or(0.0)),
710 (border[5].as_f64().unwrap_or(0.0) - border[1].as_f64().unwrap_or(0.0)),
711 );
712
713 let mut params = json!({
715 "format": opts.format.as_str(),
716 "clip": { "x": x, "y": y, "width": w, "height": h, "scale": 1.0 },
717 "fromSurface": true,
718 "captureBeyondViewport": opts.full_page,
719 });
720
721 if matches!(opts.format, ImageFormat::Jpeg | ImageFormat::WebP) {
723 params["quality"] = json!(opts.quality.unwrap_or(90));
724 }
725
726 if opts.omit_background && matches!(opts.format, ImageFormat::Png) {
728 let msg_id = next_id();
730 let msg = json!({
731 "id": msg_id,
732 "method": "Emulation.setDefaultBackgroundColorOverride",
733 "params": { "color": { "r": 0, "g": 0, "b": 0, "a": 0 } }
734 })
735 .to_string();
736 send_and_get_msg(
737 self.parent.transport.clone(),
738 msg_id,
739 &self.parent.session_id,
740 msg,
741 )
742 .await?;
743 }
744
745 let msg_id = next_id();
746 let msg_cap = json!({
747 "id": msg_id,
748 "method": "Page.captureScreenshot",
749 "params": params
750 })
751 .to_string();
752
753 self.parent.activate().await?;
754 let res_cap = send_and_get_msg(
755 self.parent.transport.clone(),
756 msg_id,
757 &self.parent.session_id,
758 msg_cap,
759 )
760 .await?;
761 let data_cap = utils::serde_msg(&res_cap)?;
762
763 if opts.omit_background && matches!(opts.format, ImageFormat::Png) {
765 let msg_id = next_id();
766 let msg = json!({
767 "id": msg_id,
768 "method": "Emulation.setDefaultBackgroundColorOverride",
769 "params": {}
770 })
771 .to_string();
772 let _ = send_and_get_msg(
773 self.parent.transport.clone(),
774 msg_id,
775 &self.parent.session_id,
776 msg,
777 )
778 .await;
779 }
780
781 data_cap["result"]["data"]
782 .as_str()
783 .map(|s| s.to_string())
784 .context("No image data received")
785 }
786
787 pub fn backend_node_id(&self) -> u64 {
789 self.backend_node_id
790 }
791 }
792}
793
794mod tab {
798 use crate::element::Element;
799 use crate::transport::{Transport, TransportResponse, next_id};
800 use crate::utils::{self, send_and_get_msg};
801 use crate::{CaptureOptions, ImageFormat, Viewport};
802 use anyhow::{Context, Result, anyhow};
803 use serde_json::{Value, json};
804 use std::sync::Arc;
805
806 pub struct Tab {
808 pub(crate) transport: Arc<Transport>,
809 pub(crate) session_id: String,
810 pub(crate) target_id: String,
811 }
812
813 impl Tab {
814 pub(crate) async fn new(transport: Arc<Transport>) -> Result<Self> {
816 let TransportResponse::Response(res_create) = transport
817 .send(json!({ "id": next_id(), "method": "Target.createTarget", "params": { "url": "about:blank" } }))
818 .await? else { return Err(anyhow!("Invalid response type")); };
819
820 let target_id = res_create.result["targetId"]
821 .as_str()
822 .context("No targetId")?
823 .to_string();
824
825 let TransportResponse::Response(res_attach) = transport
826 .send(json!({ "id": next_id(), "method": "Target.attachToTarget", "params": { "targetId": target_id } }))
827 .await? else { return Err(anyhow!("Invalid response type")); };
828
829 let session_id = res_attach.result["sessionId"]
830 .as_str()
831 .context("No sessionId")?
832 .to_string();
833
834 Ok(Self {
835 transport,
836 session_id,
837 target_id,
838 })
839 }
840
841 pub(crate) async fn send_cmd(
843 &self,
844 method: &str,
845 params: serde_json::Value,
846 ) -> Result<Value> {
847 let msg_id = next_id();
848 let msg = json!({
849 "id": msg_id,
850 "method": method,
851 "params": params
852 })
853 .to_string();
854 let res =
855 send_and_get_msg(self.transport.clone(), msg_id, &self.session_id, msg).await?;
856 utils::serde_msg(&res)
857 }
858
859 pub async fn set_viewport(&self, viewport: &Viewport) -> Result<&Self> {
872 let screen_orientation = if viewport.is_landscape {
873 json!({"type": "landscapePrimary", "angle": 90})
874 } else {
875 json!({"type": "portraitPrimary", "angle": 0})
876 };
877
878 self.send_cmd(
879 "Emulation.setDeviceMetricsOverride",
880 json!({
881 "width": viewport.width,
882 "height": viewport.height,
883 "deviceScaleFactor": viewport.device_scale_factor,
884 "mobile": viewport.is_mobile,
885 "screenOrientation": screen_orientation
886 }),
887 )
888 .await?;
889
890 if viewport.has_touch {
892 self.send_cmd(
893 "Emulation.setTouchEmulationEnabled",
894 json!({
895 "enabled": true,
896 "maxTouchPoints": 5
897 }),
898 )
899 .await?;
900 }
901
902 Ok(self)
903 }
904
905 pub async fn clear_viewport(&self) -> Result<&Self> {
907 self.send_cmd("Emulation.clearDeviceMetricsOverride", json!({}))
908 .await?;
909 Ok(self)
910 }
911
912 pub async fn set_content(&self, content: &str) -> Result<&Self> {
914 self.send_cmd("Page.enable", json!({})).await?;
916
917 let load_event_future = self
919 .transport
920 .wait_for_event(&self.session_id, "Page.loadEventFired");
921
922 let js_write = format!(
924 r#"document.open(); document.write({}); document.close();"#,
925 serde_json::to_string(content)?
926 );
927
928 self.send_cmd(
929 "Runtime.evaluate",
930 json!({
931 "expression": js_write,
932 "awaitPromise": true
933 }),
934 )
935 .await?;
936
937 load_event_future.await?;
939
940 Ok(self)
941 }
942
943 pub async fn evaluate(&self, expression: &str) -> Result<Value> {
951 let result = self
952 .send_cmd(
953 "Runtime.evaluate",
954 json!({
955 "expression": expression,
956 "returnByValue": true,
957 "awaitPromise": true
958 }),
959 )
960 .await?;
961 Ok(result["result"]["result"]["value"].clone())
962 }
963
964 pub async fn evaluate_as_string(&self, expression: &str) -> Result<String> {
966 let value = self.evaluate(expression).await?;
967 value
968 .as_str()
969 .map(|s| s.to_string())
970 .or_else(|| Some(value.to_string()))
971 .context("Failed to convert result to string")
972 }
973
974 pub async fn find_element(&self, selector: &str) -> Result<Element<'_>> {
976 let msg_id = next_id();
977 let msg_doc =
978 json!({ "id": msg_id, "method": "DOM.getDocument", "params": {} }).to_string();
979 let res_doc =
980 send_and_get_msg(self.transport.clone(), msg_id, &self.session_id, msg_doc).await?;
981 let data_doc = utils::serde_msg(&res_doc)?;
982 let root_node_id = data_doc["result"]["root"]["nodeId"]
983 .as_u64()
984 .context("No root node")?;
985
986 let msg_id = next_id();
987 let msg_sel = json!({
988 "id": msg_id,
989 "method": "DOM.querySelector",
990 "params": { "nodeId": root_node_id, "selector": selector }
991 })
992 .to_string();
993 let res_sel =
994 send_and_get_msg(self.transport.clone(), msg_id, &self.session_id, msg_sel).await?;
995 let data_sel = utils::serde_msg(&res_sel)?;
996 let node_id = data_sel["result"]["nodeId"]
997 .as_u64()
998 .context("Element not found")?;
999
1000 Element::new(self, node_id).await
1001 }
1002
1003 pub async fn wait_for_selector(
1009 &self,
1010 selector: &str,
1011 timeout_ms: u64,
1012 ) -> Result<Element<'_>> {
1013 let start = std::time::Instant::now();
1014 let timeout = std::time::Duration::from_millis(timeout_ms);
1015
1016 loop {
1017 match self.find_element(selector).await {
1018 Ok(element) => return Ok(element),
1019 Err(_) if start.elapsed() < timeout => {
1020 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1021 }
1022 Err(e) => return Err(e),
1023 }
1024 }
1025 }
1026
1027 pub async fn screenshot(&self, opts: CaptureOptions) -> Result<String> {
1038 if let Some(ref viewport) = opts.viewport {
1040 self.set_viewport(viewport).await?;
1041 }
1042
1043 let mut params = json!({
1044 "format": opts.format.as_str(),
1045 "fromSurface": true,
1046 "captureBeyondViewport": opts.full_page,
1047 });
1048
1049 if matches!(opts.format, ImageFormat::Jpeg | ImageFormat::WebP) {
1050 params["quality"] = json!(opts.quality.unwrap_or(90));
1051 }
1052
1053 if let Some(ref clip) = opts.clip {
1054 params["clip"] = json!({
1055 "x": clip.x,
1056 "y": clip.y,
1057 "width": clip.width,
1058 "height": clip.height,
1059 "scale": clip.scale
1060 });
1061 }
1062
1063 if opts.omit_background && matches!(opts.format, ImageFormat::Png) {
1064 self.send_cmd(
1065 "Emulation.setDefaultBackgroundColorOverride",
1066 json!({ "color": { "r": 0, "g": 0, "b": 0, "a": 0 } }),
1067 )
1068 .await?;
1069 }
1070
1071 self.activate().await?;
1072
1073 let result = self.send_cmd("Page.captureScreenshot", params).await?;
1074
1075 if opts.omit_background && matches!(opts.format, ImageFormat::Png) {
1076 let _ = self
1077 .send_cmd("Emulation.setDefaultBackgroundColorOverride", json!({}))
1078 .await;
1079 }
1080
1081 result["result"]["data"]
1082 .as_str()
1083 .map(|s| s.to_string())
1084 .context("No image data received")
1085 }
1086
1087 pub async fn activate(&self) -> Result<&Self> {
1089 let msg_id = next_id();
1090 let msg = json!({ "id": msg_id, "method": "Target.activateTarget", "params": { "targetId": self.target_id } }).to_string();
1091 send_and_get_msg(self.transport.clone(), msg_id, &self.session_id, msg).await?;
1092 Ok(self)
1093 }
1094
1095 pub async fn goto(&self, url: &str) -> Result<&Self> {
1097 self.send_cmd("Page.enable", json!({})).await?;
1098
1099 let load_event_future = self
1100 .transport
1101 .wait_for_event(&self.session_id, "Page.loadEventFired");
1102
1103 let msg_id = next_id();
1104 let msg = json!({ "id": msg_id, "method": "Page.navigate", "params": { "url": url } })
1105 .to_string();
1106 send_and_get_msg(self.transport.clone(), msg_id, &self.session_id, msg).await?;
1107
1108 load_event_future.await?;
1109 Ok(self)
1110 }
1111
1112 pub async fn goto_no_wait(&self, url: &str) -> Result<&Self> {
1114 let msg_id = next_id();
1115 let msg = json!({ "id": msg_id, "method": "Page.navigate", "params": { "url": url } })
1116 .to_string();
1117 send_and_get_msg(self.transport.clone(), msg_id, &self.session_id, msg).await?;
1118 Ok(self)
1119 }
1120
1121 pub async fn reload(&self) -> Result<&Self> {
1123 self.send_cmd("Page.enable", json!({})).await?;
1124
1125 let load_event_future = self
1126 .transport
1127 .wait_for_event(&self.session_id, "Page.loadEventFired");
1128
1129 self.send_cmd("Page.reload", json!({})).await?;
1130
1131 load_event_future.await?;
1132 Ok(self)
1133 }
1134
1135 pub async fn url(&self) -> Result<String> {
1137 self.evaluate_as_string("window.location.href").await
1138 }
1139
1140 pub async fn title(&self) -> Result<String> {
1142 self.evaluate_as_string("document.title").await
1143 }
1144
1145 pub fn session_id(&self) -> &str {
1147 &self.session_id
1148 }
1149
1150 pub fn target_id(&self) -> &str {
1152 &self.target_id
1153 }
1154
1155 pub async fn close(&self) -> Result<()> {
1157 let msg_id = next_id();
1158 let msg = json!({ "id": msg_id, "method": "Target.closeTarget", "params": { "targetId": self.target_id } }).to_string();
1159 send_and_get_msg(self.transport.clone(), msg_id, &self.session_id, msg).await?;
1160 Ok(())
1161 }
1162 }
1163}
1164
1165mod browser {
1169 use crate::transport::{Transport, TransportResponse, next_id};
1170 use crate::{CaptureOptions, Tab, Viewport};
1171 use anyhow::{Context, Result, anyhow};
1172 use rand::{Rng, thread_rng};
1173 use regex::Regex;
1174 use serde_json::json;
1175 use std::io::{BufRead, BufReader};
1176 use std::path::{Path, PathBuf};
1177 use std::process::{Child, Command, Stdio};
1178 use std::sync::Arc;
1179 use std::time::Duration;
1180 use tokio::sync::Mutex;
1181 use which::which;
1182
1183 struct CustomTempDir {
1185 path: PathBuf,
1186 }
1187
1188 impl CustomTempDir {
1189 fn new(base: PathBuf, prefix: &str) -> Result<Self> {
1191 std::fs::create_dir_all(&base)?;
1192 let name = format!(
1193 "{}_{}_{}",
1194 prefix,
1195 chrono::Local::now().format("%Y%m%d_%H%M%S"),
1196 thread_rng()
1197 .sample_iter(&rand::distributions::Alphanumeric)
1198 .take(6)
1199 .map(char::from)
1200 .collect::<String>()
1201 );
1202 let path = base.join(name);
1203 std::fs::create_dir(&path)?;
1204 Ok(Self { path })
1205 }
1206 }
1207
1208 impl Drop for CustomTempDir {
1209 fn drop(&mut self) {
1210 for i in 0..10 {
1213 if std::fs::remove_dir_all(&self.path).is_ok() {
1214 return;
1215 }
1216 std::thread::sleep(Duration::from_millis(100 * (i as u64 + 1).min(3)));
1218 }
1219 let _ = std::fs::remove_dir_all(&self.path);
1221 }
1222 }
1223
1224 struct BrowserProcess {
1226 child: Child,
1227 _temp: CustomTempDir,
1228 }
1229
1230 impl Drop for BrowserProcess {
1231 fn drop(&mut self) {
1232 let _ = self.child.kill();
1233 let _ = self.child.wait();
1234 std::thread::sleep(Duration::from_millis(200));
1237 }
1238 }
1239
1240 #[derive(Clone)]
1241 pub struct Browser {
1242 transport: Arc<Transport>,
1243 process: Arc<Mutex<Option<BrowserProcess>>>,
1244 }
1245
1246 static GLOBAL_BROWSER: Mutex<Option<Browser>> = Mutex::const_new(None);
1247
1248 impl Browser {
1249 pub async fn new() -> Result<Self> {
1251 Self::launch(true).await
1252 }
1253
1254 pub async fn new_with_head() -> Result<Self> {
1256 Self::launch(false).await
1257 }
1258
1259 async fn launch(headless: bool) -> Result<Self> {
1261 let temp = CustomTempDir::new(std::env::current_dir()?.join("temp"), "cdp-shot")?;
1262 let exe = Self::find_chrome()?;
1263 let port = (8000..9000)
1264 .find(|&p| std::net::TcpListener::bind(("127.0.0.1", p)).is_ok())
1265 .ok_or(anyhow!("No available port"))?;
1266
1267 let mut args = vec![
1268 format!("--remote-debugging-port={}", port),
1269 format!("--user-data-dir={}", temp.path.display()),
1270 "--no-sandbox".into(), "--no-zygote".into(), "--in-process-gpu".into(),
1271 "--disable-dev-shm-usage".into(), "--disable-background-networking".into(),
1272 "--disable-default-apps".into(), "--disable-extensions".into(),
1273 "--disable-sync".into(), "--disable-translate".into(),
1274 "--metrics-recording-only".into(), "--safebrowsing-disable-auto-update".into(),
1275 "--mute-audio".into(), "--no-first-run".into(), "--hide-scrollbars".into(),
1276 "--window-size=1200,1600".into(),
1277 "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36".into()
1278 ];
1279 if headless {
1280 args.push("--headless=new".into());
1281 }
1282
1283 #[cfg(windows)]
1284 let mut cmd = {
1285 use std::os::windows::process::CommandExt;
1286 let mut c = Command::new(&exe);
1287 c.creation_flags(0x08000000);
1288 c
1289 };
1290 #[cfg(not(windows))]
1291 let mut cmd = Command::new(&exe);
1292
1293 let mut child = cmd.args(args).stderr(Stdio::piped()).spawn()?;
1294 let stderr = child.stderr.take().context("No stderr")?;
1295 let ws_url = Self::wait_for_ws(stderr).await?;
1296
1297 Ok(Self {
1298 transport: Arc::new(Transport::new(&ws_url).await?),
1299 process: Arc::new(Mutex::new(Some(BrowserProcess { child, _temp: temp }))),
1300 })
1301 }
1302
1303 fn find_chrome() -> Result<PathBuf> {
1305 if let Ok(p) = std::env::var("CHROME") {
1306 return Ok(p.into());
1307 }
1308 let apps = [
1309 "google-chrome-stable",
1310 "chromium",
1311 "chrome",
1312 "msedge",
1313 "microsoft-edge",
1314 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1315 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
1316 ];
1317 for app in apps {
1318 if let Ok(p) = which(app) {
1319 return Ok(p);
1320 }
1321 if Path::new(app).exists() {
1322 return Ok(app.into());
1323 }
1324 }
1325
1326 #[cfg(windows)]
1327 {
1328 use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
1329 let keys = [
1330 r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe",
1331 r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe",
1332 ];
1333 for k in keys {
1334 if let Ok(rk) = RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey(k)
1335 && let Ok(v) = rk.get_value::<String, _>("")
1336 {
1337 return Ok(v.into());
1338 }
1339 }
1340 let paths = [
1341 r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
1342 r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
1343 r"C:\Program Files\Google\Chrome\Application\chrome.exe",
1344 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
1345 ];
1346 for p in paths {
1347 if Path::new(p).exists() {
1348 return Ok(p.into());
1349 }
1350 }
1351 }
1352 Err(anyhow!("Chrome/Edge not found. Set CHROME env var."))
1353 }
1354
1355 async fn wait_for_ws(stderr: std::process::ChildStderr) -> Result<String> {
1357 let reader = BufReader::new(stderr);
1358 let re = Regex::new(r"listening on (.*/devtools/browser/.*)$")?;
1359 tokio::task::spawn_blocking(move || {
1360 for line in reader.lines() {
1361 let l = line?;
1362 if let Some(cap) = re.captures(&l) {
1363 return Ok(cap[1].to_string());
1364 }
1365 }
1366 Err(anyhow!("WS URL not found in stderr"))
1367 })
1368 .await?
1369 }
1370
1371 pub async fn new_tab(&self) -> Result<Tab> {
1373 Tab::new(self.transport.clone()).await
1374 }
1375
1376 pub async fn capture_html(&self, html: &str, selector: &str) -> Result<String> {
1378 self.capture_html_with_options(html, selector, CaptureOptions::default())
1379 .await
1380 }
1381
1382 pub async fn capture_html_with_options(
1393 &self,
1394 html: &str,
1395 selector: &str,
1396 opts: CaptureOptions,
1397 ) -> Result<String> {
1398 let tab = self.new_tab().await?;
1399
1400 if let Some(ref viewport) = opts.viewport {
1402 tab.set_viewport(viewport).await?;
1403 }
1404
1405 tab.set_content(html).await?;
1406 let el = tab.find_element(selector).await?;
1407 let shot = el.screenshot_with_options(opts).await?;
1408 let _ = tab.close().await;
1409 Ok(shot)
1410 }
1411
1412 pub async fn capture_html_hidpi(
1422 &self,
1423 html: &str,
1424 selector: &str,
1425 scale: f64,
1426 ) -> Result<String> {
1427 let opts = CaptureOptions::new()
1428 .with_viewport(Viewport::default().with_device_scale_factor(scale));
1429 self.capture_html_with_options(html, selector, opts).await
1430 }
1431
1432 pub async fn close_async(&self) -> Result<()> {
1434 self.transport.shutdown().await;
1435 let mut lock = self.process.lock().await;
1436 if let Some(_proc) = lock.take() {
1437 }
1439 Ok(())
1440 }
1441
1442 async fn is_alive(&self) -> bool {
1444 self.transport
1445 .send(json!({
1446 "id": next_id(),
1447 "method": "Target.getTargets",
1448 "params": {}
1449 }))
1450 .await
1451 .is_ok()
1452 }
1453
1454 pub async fn instance() -> Self {
1457 let mut lock = GLOBAL_BROWSER.lock().await;
1458
1459 if let Some(b) = &*lock {
1461 if b.is_alive().await {
1462 return b.clone();
1463 }
1464 log::warn!("[cdp-html-shot] Browser instance died, recreating...");
1466 let _ = b.close_async().await;
1467 }
1468
1469 let b = Self::new().await.expect("Init global browser failed");
1471
1472 if let Ok(TransportResponse::Response(res)) = b
1474 .transport
1475 .send(json!({"id": next_id(), "method":"Target.getTargets", "params":{}}))
1476 .await
1477 && let Some(list) = res.result["targetInfos"].as_array()
1478 && let Some(id) = list
1479 .iter()
1480 .find(|t| t["type"] == "page")
1481 .and_then(|t| t["targetId"].as_str())
1482 {
1483 let _ = b.transport.send(json!({"id":next_id(), "method":"Target.closeTarget", "params":{"targetId":id}})).await;
1484 }
1485
1486 *lock = Some(b.clone());
1487 b
1488 }
1489 }
1490}
1491
1492#[cfg(feature = "atexit")]
1496mod exit_hook {
1497 use std::sync::{Arc, Once};
1498
1499 pub struct ExitHook {
1501 func: Arc<dyn Fn() + Send + Sync>,
1502 }
1503
1504 impl ExitHook {
1505 pub fn new<F: Fn() + Send + Sync + 'static>(f: F) -> Self {
1507 Self { func: Arc::new(f) }
1508 }
1509
1510 pub fn register(&self) -> Result<(), Box<dyn std::error::Error>> {
1512 static ONCE: Once = Once::new();
1513 let f = self.func.clone();
1514 let res = Ok(());
1515 ONCE.call_once(|| {
1516 if let Err(e) = ctrlc::set_handler(move || {
1517 f();
1518 std::process::exit(0);
1519 }) {
1520 eprintln!("Ctrl+C handler error: {}", e);
1521 }
1522 });
1523 res
1524 }
1525 }
1526
1527 impl Drop for ExitHook {
1528 fn drop(&mut self) {
1529 (self.func)();
1530 }
1531 }
1532}