1use std::fmt::{Display, Formatter};
2use std::sync::{Arc, Mutex, OnceLock};
3
4use crate::proto::browser_session_command::Command as BrowserCommand;
5use crate::proto::browser_session_event::Event as BrowserEvent;
6use crate::proto::engine_service_client::EngineServiceClient;
7use crate::proto::tab_session_command::Command as TabCommand;
8use crate::proto::tab_session_event::Event as TabEvent;
9use crate::proto::{
10 BrowserSessionCommand, BrowserSessionEvent, ClickElementCommand, CloseBrowserSessionCommand,
11 CloseTabSessionCommand, CommandRetryOptions, CountElementsCommand, ElementCountedEvent,
12 ElementsHighlightedEvent, FillElementCommand, FocusElementCommand, GetInnerTextCommand,
13 GetTextContentCommand, HighlightElementsCommand, HoverElementCommand, LaunchChromeCommand,
14 NavigateTabCommand, OpenTabCommand, PingRequest, PressKeyCommand, SessionPingCommand,
15 TabSessionCommand, TabSessionEvent, TabSessionPingCommand, WaitForSelectorCommand,
16};
17use tokio::sync::{Mutex as AsyncMutex, mpsc};
18use tokio_stream::wrappers::ReceiverStream;
19use tonic::transport::Channel;
20
21const DEFAULT_SERVER_ADDR: &str = "http://127.0.0.1:50051";
22const SERVER_ADDR_ENV_VAR: &str = "ALLWRIGHT_SERVER_ADDR";
23
24type Result<T> = std::result::Result<T, Error>;
25
26static RUNTIME: OnceLock<Mutex<Option<Arc<RuntimeClient>>>> = OnceLock::new();
27static SERVER_ADDR_OVERRIDE: OnceLock<Mutex<Option<String>>> = OnceLock::new();
28
29#[derive(Debug)]
30pub struct Error {
31 message: String,
32}
33
34impl Error {
35 fn new(message: impl Into<String>) -> Self {
36 Self {
37 message: message.into(),
38 }
39 }
40}
41
42impl Display for Error {
43 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
44 f.write_str(&self.message)
45 }
46}
47
48impl std::error::Error for Error {}
49
50impl From<tonic::transport::Error> for Error {
51 fn from(value: tonic::transport::Error) -> Self {
52 Self::new(format!("transport error: {value}"))
53 }
54}
55
56impl From<tonic::Status> for Error {
57 fn from(value: tonic::Status) -> Self {
58 Self::new(format!("grpc status error: {value}"))
59 }
60}
61
62#[derive(Debug, Clone, Default)]
63pub struct LaunchOptions {
64 pub chrome_binary: Option<String>,
65 pub timeout_ms: Option<u32>,
66}
67
68#[derive(Debug, Clone, Default)]
69pub struct CommandOptions {
70 pub timeout_ms: Option<u32>,
71}
72
73#[derive(Debug, Clone)]
74pub struct NavigateResult {
75 pub url: String,
76 pub note: String,
77 pub bidi_session_id: String,
78 pub mapper_target_id: String,
79 pub mapper_session_id: String,
80 pub package_version: String,
81}
82
83#[derive(Debug, Clone)]
84pub struct ClickResult {
85 pub selector: String,
86 pub note: String,
87 pub bidi_session_id: String,
88}
89
90#[derive(Debug, Clone)]
91pub struct CountResult {
92 pub selector: String,
93 pub count: u32,
94 pub note: String,
95}
96
97#[derive(Debug, Clone, Default)]
98pub struct HighlightOptions {
99 pub timeout_ms: Option<u32>,
100 pub duration_ms: Option<u32>,
101}
102
103#[derive(Debug, Clone)]
104pub struct HighlightResult {
105 pub selector: String,
106 pub count: u32,
107 pub note: String,
108}
109
110#[derive(Debug, Clone)]
111pub struct ElementResult {
112 pub selector: String,
113 pub note: String,
114}
115
116#[derive(Debug, Clone)]
117pub struct FillResult {
118 pub selector: String,
119 pub value: String,
120 pub note: String,
121}
122
123#[derive(Debug, Clone)]
124pub struct PressResult {
125 pub selector: String,
126 pub key: String,
127 pub note: String,
128}
129
130#[derive(Debug, Clone)]
131pub struct TextResult {
132 pub selector: String,
133 pub text: String,
134 pub note: String,
135}
136
137#[derive(Debug, Clone, Default)]
138pub struct PressOptions {
139 pub timeout_ms: Option<u32>,
140 pub text: Option<String>,
141}
142
143#[derive(Debug, Clone, Default)]
144pub struct WaitForSelectorOptions {
145 pub timeout_ms: Option<u32>,
146 pub visible: Option<bool>,
147}
148
149#[derive(Debug, Clone)]
150pub struct WaitForSelectorResult {
151 pub selector: String,
152 pub visible: bool,
153 pub note: String,
154}
155
156#[derive(Clone)]
157pub struct Browser {
158 inner: Arc<BrowserInner>,
159}
160
161#[derive(Clone)]
162pub struct Tab {
163 inner: Arc<TabInner>,
164}
165
166#[derive(Clone)]
167struct RuntimeClient {
168 engine: EngineServiceClient<Channel>,
169}
170
171struct BrowserInner {
172 runtime: Arc<RuntimeClient>,
173 state: AsyncMutex<BrowserState>,
174 session_id: String,
175 browser_name: String,
176 launch_note: String,
177 cdp_websocket_url: String,
178 user_data_dir: String,
179 initial_tab: Tab,
180}
181
182struct BrowserState {
183 command_tx: mpsc::Sender<BrowserSessionCommand>,
184 events: tonic::Streaming<BrowserSessionEvent>,
185 closed: bool,
186}
187
188struct TabInner {
189 runtime: Arc<RuntimeClient>,
190 browser_session_id: String,
191 session_id: String,
192 state: AsyncMutex<TabState>,
193}
194
195#[derive(Default)]
196struct TabState {
197 handle: Option<TabHandle>,
198}
199
200struct TabHandle {
201 command_tx: mpsc::Sender<TabSessionCommand>,
202 events: tonic::Streaming<TabSessionEvent>,
203 closed: bool,
204}
205
206pub async fn ping() -> Result<String> {
207 let runtime = get_runtime().await?;
208 let mut engine = runtime.engine.clone();
209 let response = engine.ping(tonic::Request::new(PingRequest {})).await?;
210 Ok(response.into_inner().message)
211}
212
213pub async fn launch_chrome(options: LaunchOptions) -> Result<Browser> {
214 let runtime = get_runtime().await?;
215 let mut engine = runtime.engine.clone();
216 let (command_tx, command_rx) = mpsc::channel(16);
217 let response = engine
218 .browser_session(tonic::Request::new(ReceiverStream::new(command_rx)))
219 .await?;
220 let mut events = response.into_inner();
221
222 command_tx
223 .send(BrowserSessionCommand {
224 command: Some(BrowserCommand::LaunchChrome(LaunchChromeCommand {
225 chrome_binary: options.chrome_binary,
226 retry_options: command_retry_options(options.timeout_ms),
227 })),
228 })
229 .await
230 .map_err(|_| Error::new("failed to send LaunchChromeCommand to browser session"))?;
231
232 loop {
233 let event = events
234 .message()
235 .await?
236 .ok_or_else(|| Error::new("browser session closed before launch response"))?;
237
238 match event.event {
239 Some(BrowserEvent::ChromeLaunched(launched)) => {
240 let browser_session_id = event.session_id;
241 let initial_tab = Tab {
242 inner: Arc::new(TabInner {
243 runtime: Arc::clone(&runtime),
244 browser_session_id: browser_session_id.clone(),
245 session_id: launched.initial_tab_session_id.clone(),
246 state: AsyncMutex::new(TabState::default()),
247 }),
248 };
249 return Ok(Browser {
250 inner: Arc::new(BrowserInner {
251 runtime,
252 state: AsyncMutex::new(BrowserState {
253 command_tx,
254 events,
255 closed: false,
256 }),
257 session_id: browser_session_id,
258 browser_name: launched.browser,
259 launch_note: launched.note,
260 cdp_websocket_url: launched.cdp_websocket_url,
261 user_data_dir: launched.user_data_dir,
262 initial_tab,
263 }),
264 });
265 }
266 Some(BrowserEvent::Error(error)) => {
267 return Err(Error::new(format!(
268 "browser session error during launch: {}",
269 error.message
270 )));
271 }
272 _ => {}
273 }
274 }
275}
276
277pub fn set_server_addr(server_addr: impl Into<String>) -> Result<()> {
278 let normalized = normalize_server_addr(&server_addr.into());
279 let mut override_slot = server_addr_override_slot()
280 .lock()
281 .map_err(|_| Error::new("server address override lock is poisoned"))?;
282 *override_slot = Some(normalized);
283 drop(override_slot);
284
285 let mut runtime = runtime_slot()
286 .lock()
287 .map_err(|_| Error::new("runtime singleton lock is poisoned"))?;
288 *runtime = None;
289 Ok(())
290}
291
292pub async fn shutdown() {
293 if let Ok(mut runtime) = runtime_slot().lock() {
294 *runtime = None;
295 }
296}
297
298impl Browser {
299 pub fn session_id(&self) -> &str {
300 &self.inner.session_id
301 }
302
303 pub fn browser_name(&self) -> &str {
304 &self.inner.browser_name
305 }
306
307 pub fn launch_note(&self) -> &str {
308 &self.inner.launch_note
309 }
310
311 pub fn cdp_websocket_url(&self) -> &str {
312 &self.inner.cdp_websocket_url
313 }
314
315 pub fn user_data_dir(&self) -> &str {
316 &self.inner.user_data_dir
317 }
318
319 pub fn initial_tab(&self) -> Tab {
320 self.inner.initial_tab.clone()
321 }
322
323 pub async fn new_tab(&self) -> Result<Tab> {
324 self.new_tab_with_options(CommandOptions::default()).await
325 }
326
327 pub async fn new_tab_with_options(&self, options: CommandOptions) -> Result<Tab> {
328 let mut state = self.inner.state.lock().await;
329 if state.closed {
330 return Err(Error::new(format!(
331 "browser session {} is closed",
332 self.inner.session_id
333 )));
334 }
335
336 state
337 .command_tx
338 .send(BrowserSessionCommand {
339 command: Some(BrowserCommand::OpenTab(OpenTabCommand {
340 retry_options: command_retry_options(options.timeout_ms),
341 })),
342 })
343 .await
344 .map_err(|_| Error::new("failed to send OpenTabCommand to browser session"))?;
345
346 loop {
347 let event =
348 state.events.message().await?.ok_or_else(|| {
349 Error::new("browser session closed while waiting for new tab")
350 })?;
351
352 match event.event {
353 Some(BrowserEvent::TabOpened(opened)) => {
354 return Ok(Tab {
355 inner: Arc::new(TabInner {
356 runtime: Arc::clone(&self.inner.runtime),
357 browser_session_id: self.inner.session_id.clone(),
358 session_id: opened.tab_session_id,
359 state: AsyncMutex::new(TabState::default()),
360 }),
361 });
362 }
363 Some(BrowserEvent::Error(error)) => {
364 return Err(Error::new(format!(
365 "browser session error while opening tab: {}",
366 error.message
367 )));
368 }
369 _ => {}
370 }
371 }
372 }
373
374 pub async fn ping(&self, message: impl Into<String>) -> Result<String> {
375 let mut state = self.inner.state.lock().await;
376 if state.closed {
377 return Err(Error::new(format!(
378 "browser session {} is closed",
379 self.inner.session_id
380 )));
381 }
382
383 state
384 .command_tx
385 .send(BrowserSessionCommand {
386 command: Some(BrowserCommand::Ping(SessionPingCommand {
387 message: message.into(),
388 })),
389 })
390 .await
391 .map_err(|_| Error::new("failed to send SessionPingCommand to browser session"))?;
392
393 loop {
394 let event = state
395 .events
396 .message()
397 .await?
398 .ok_or_else(|| Error::new("browser session closed while waiting for pong"))?;
399
400 match event.event {
401 Some(BrowserEvent::Pong(pong)) => return Ok(pong.message),
402 Some(BrowserEvent::Error(error)) => {
403 return Err(Error::new(format!(
404 "browser session error while pinging: {}",
405 error.message
406 )));
407 }
408 _ => {}
409 }
410 }
411 }
412
413 pub async fn close(&self) -> Result<()> {
414 let mut state = self.inner.state.lock().await;
415 if state.closed {
416 return Ok(());
417 }
418
419 state
420 .command_tx
421 .send(BrowserSessionCommand {
422 command: Some(BrowserCommand::Close(CloseBrowserSessionCommand {})),
423 })
424 .await
425 .map_err(|_| Error::new("failed to send CloseBrowserSessionCommand"))?;
426
427 loop {
428 let event =
429 state.events.message().await?.ok_or_else(|| {
430 Error::new("browser session closed before close confirmation")
431 })?;
432
433 match event.event {
434 Some(BrowserEvent::Closed(_)) => {
435 state.closed = true;
436 return Ok(());
437 }
438 Some(BrowserEvent::Error(error)) => {
439 return Err(Error::new(format!(
440 "browser session error while closing: {}",
441 error.message
442 )));
443 }
444 _ => {}
445 }
446 }
447 }
448}
449
450impl Tab {
451 pub fn session_id(&self) -> &str {
452 &self.inner.session_id
453 }
454
455 pub async fn ping(&self, message: impl Into<String>) -> Result<String> {
456 let mut state = self.inner.state.lock().await;
457 let handle = self.ensure_handle(&mut state).await?;
458 if handle.closed {
459 return Err(Error::new(format!(
460 "tab session {} is closed",
461 self.inner.session_id
462 )));
463 }
464
465 handle
466 .command_tx
467 .send(TabSessionCommand {
468 browser_session_id: self.inner.browser_session_id.clone(),
469 tab_session_id: self.inner.session_id.clone(),
470 command: Some(TabCommand::Ping(TabSessionPingCommand {
471 message: message.into(),
472 })),
473 })
474 .await
475 .map_err(|_| Error::new("failed to send TabSessionPingCommand"))?;
476
477 loop {
478 let event = handle
479 .events
480 .message()
481 .await?
482 .ok_or_else(|| Error::new("tab session closed while waiting for pong"))?;
483
484 match event.event {
485 Some(TabEvent::Attached(_)) => {}
486 Some(TabEvent::Pong(pong)) => return Ok(pong.message),
487 Some(TabEvent::Error(error)) => {
488 return Err(Error::new(format!(
489 "tab session error while pinging: {}",
490 error.message
491 )));
492 }
493 Some(TabEvent::Closed(_)) => {
494 handle.closed = true;
495 return Err(Error::new(format!(
496 "tab session {} closed while waiting for pong",
497 self.inner.session_id
498 )));
499 }
500 _ => {}
501 }
502 }
503 }
504
505 pub async fn navigate(&self, url: impl Into<String>) -> Result<NavigateResult> {
506 self.navigate_with_options(url, CommandOptions::default())
507 .await
508 }
509
510 pub async fn navigate_with_options(
511 &self,
512 url: impl Into<String>,
513 options: CommandOptions,
514 ) -> Result<NavigateResult> {
515 let mut state = self.inner.state.lock().await;
516 let handle = self.ensure_handle(&mut state).await?;
517 if handle.closed {
518 return Err(Error::new(format!(
519 "tab session {} is closed",
520 self.inner.session_id
521 )));
522 }
523
524 handle
525 .command_tx
526 .send(TabSessionCommand {
527 browser_session_id: self.inner.browser_session_id.clone(),
528 tab_session_id: self.inner.session_id.clone(),
529 command: Some(TabCommand::Navigate(NavigateTabCommand {
530 url: url.into(),
531 retry_options: command_retry_options(options.timeout_ms),
532 })),
533 })
534 .await
535 .map_err(|_| Error::new("failed to send NavigateTabCommand"))?;
536
537 let mut navigated = None;
538 let mut injection = None;
539
540 loop {
541 let event = handle
542 .events
543 .message()
544 .await?
545 .ok_or_else(|| Error::new("tab session closed while waiting for navigation"))?;
546
547 match event.event {
548 Some(TabEvent::Attached(_)) => {}
549 Some(TabEvent::Navigated(navigated_event)) => {
550 navigated = Some(navigated_event);
551 }
552 Some(TabEvent::ChromiumBidiInjection(injection_event)) => {
553 injection = Some(injection_event);
554 }
555 Some(TabEvent::Error(error)) => {
556 return Err(Error::new(format!(
557 "tab session error while navigating: {}",
558 error.message
559 )));
560 }
561 Some(TabEvent::Closed(_)) => {
562 handle.closed = true;
563 return Err(Error::new(format!(
564 "tab session {} closed while navigating",
565 self.inner.session_id
566 )));
567 }
568 _ => {}
569 }
570
571 if let (Some(navigated_event), Some(injection_event)) =
572 (navigated.take(), injection.take())
573 {
574 return Ok(NavigateResult {
575 url: navigated_event.url,
576 note: navigated_event.note,
577 bidi_session_id: injection_event.bidi_session_id,
578 mapper_target_id: injection_event.mapper_target_id,
579 mapper_session_id: injection_event.mapper_session_id,
580 package_version: injection_event.package_version,
581 });
582 }
583 }
584 }
585
586 pub async fn click(&self, css_selector: impl Into<String>) -> Result<ClickResult> {
587 self.click_with_options(css_selector, CommandOptions::default())
588 .await
589 }
590
591 pub async fn click_with_options(
592 &self,
593 css_selector: impl Into<String>,
594 options: CommandOptions,
595 ) -> Result<ClickResult> {
596 let mut state = self.inner.state.lock().await;
597 let handle = self.ensure_handle(&mut state).await?;
598 if handle.closed {
599 return Err(Error::new(format!(
600 "tab session {} is closed",
601 self.inner.session_id
602 )));
603 }
604
605 handle
606 .command_tx
607 .send(TabSessionCommand {
608 browser_session_id: self.inner.browser_session_id.clone(),
609 tab_session_id: self.inner.session_id.clone(),
610 command: Some(TabCommand::ClickElement(ClickElementCommand {
611 css_selector: css_selector.into(),
612 retry_options: command_retry_options(options.timeout_ms),
613 })),
614 })
615 .await
616 .map_err(|_| Error::new("failed to send ClickElementCommand"))?;
617
618 loop {
619 let event =
620 handle.events.message().await?.ok_or_else(|| {
621 Error::new("tab session closed while waiting for click result")
622 })?;
623
624 match event.event {
625 Some(TabEvent::Attached(_)) => {}
626 Some(TabEvent::ElementClicked(clicked)) => {
627 return Ok(ClickResult {
628 selector: clicked.css_selector,
629 note: clicked.note,
630 bidi_session_id: clicked.bidi_session_id,
631 });
632 }
633 Some(TabEvent::Error(error)) => {
634 return Err(Error::new(format!(
635 "tab session error while clicking: {}",
636 error.message
637 )));
638 }
639 Some(TabEvent::Closed(_)) => {
640 handle.closed = true;
641 return Err(Error::new(format!(
642 "tab session {} closed while waiting for click result",
643 self.inner.session_id
644 )));
645 }
646 _ => {}
647 }
648 }
649 }
650
651 pub async fn count(&self, css_selector: impl Into<String>) -> Result<CountResult> {
652 self.count_with_options(css_selector, CommandOptions::default())
653 .await
654 }
655
656 pub async fn count_with_options(
657 &self,
658 css_selector: impl Into<String>,
659 options: CommandOptions,
660 ) -> Result<CountResult> {
661 let mut state = self.inner.state.lock().await;
662 let handle = self.ensure_handle(&mut state).await?;
663 if handle.closed {
664 return Err(Error::new(format!(
665 "tab session {} is closed",
666 self.inner.session_id
667 )));
668 }
669
670 handle
671 .command_tx
672 .send(TabSessionCommand {
673 browser_session_id: self.inner.browser_session_id.clone(),
674 tab_session_id: self.inner.session_id.clone(),
675 command: Some(TabCommand::CountElements(CountElementsCommand {
676 css_selector: css_selector.into(),
677 retry_options: command_retry_options(options.timeout_ms),
678 })),
679 })
680 .await
681 .map_err(|_| Error::new("failed to send CountElementsCommand"))?;
682
683 loop {
684 let event =
685 handle.events.message().await?.ok_or_else(|| {
686 Error::new("tab session closed while waiting for count result")
687 })?;
688
689 match event.event {
690 Some(TabEvent::Attached(_)) => {}
691 Some(TabEvent::ElementCounted(counted)) => {
692 return Ok(count_result_from_event(counted));
693 }
694 Some(TabEvent::Error(error)) => {
695 return Err(Error::new(format!(
696 "tab session error while counting elements: {}",
697 error.message
698 )));
699 }
700 Some(TabEvent::Closed(_)) => {
701 handle.closed = true;
702 return Err(Error::new(format!(
703 "tab session {} closed while waiting for count result",
704 self.inner.session_id
705 )));
706 }
707 _ => {}
708 }
709 }
710 }
711
712 pub async fn highlight(&self, css_selector: impl Into<String>) -> Result<HighlightResult> {
713 self.highlight_with_options(css_selector, HighlightOptions::default())
714 .await
715 }
716
717 pub async fn highlight_with_options(
718 &self,
719 css_selector: impl Into<String>,
720 options: HighlightOptions,
721 ) -> Result<HighlightResult> {
722 let mut state = self.inner.state.lock().await;
723 let handle = self.ensure_handle(&mut state).await?;
724 if handle.closed {
725 return Err(Error::new(format!(
726 "tab session {} is closed",
727 self.inner.session_id
728 )));
729 }
730
731 handle
732 .command_tx
733 .send(TabSessionCommand {
734 browser_session_id: self.inner.browser_session_id.clone(),
735 tab_session_id: self.inner.session_id.clone(),
736 command: Some(TabCommand::HighlightElements(HighlightElementsCommand {
737 css_selector: css_selector.into(),
738 duration_ms: options.duration_ms,
739 retry_options: command_retry_options(options.timeout_ms),
740 })),
741 })
742 .await
743 .map_err(|_| Error::new("failed to send HighlightElementsCommand"))?;
744
745 loop {
746 let event = handle.events.message().await?.ok_or_else(|| {
747 Error::new("tab session closed while waiting for highlight result")
748 })?;
749
750 match event.event {
751 Some(TabEvent::Attached(_)) => {}
752 Some(TabEvent::ElementsHighlighted(highlighted)) => {
753 return Ok(highlight_result_from_event(highlighted));
754 }
755 Some(TabEvent::Error(error)) => {
756 return Err(Error::new(format!(
757 "tab session error while highlighting elements: {}",
758 error.message
759 )));
760 }
761 Some(TabEvent::Closed(_)) => {
762 handle.closed = true;
763 return Err(Error::new(format!(
764 "tab session {} closed while waiting for highlight result",
765 self.inner.session_id
766 )));
767 }
768 _ => {}
769 }
770 }
771 }
772
773 pub async fn focus(&self, css_selector: impl Into<String>) -> Result<ElementResult> {
774 self.focus_with_options(css_selector, CommandOptions::default())
775 .await
776 }
777
778 pub async fn focus_with_options(
779 &self,
780 css_selector: impl Into<String>,
781 options: CommandOptions,
782 ) -> Result<ElementResult> {
783 let mut state = self.inner.state.lock().await;
784 let handle = self.ensure_handle(&mut state).await?;
785 if handle.closed {
786 return Err(Error::new(format!(
787 "tab session {} is closed",
788 self.inner.session_id
789 )));
790 }
791 handle
792 .command_tx
793 .send(TabSessionCommand {
794 browser_session_id: self.inner.browser_session_id.clone(),
795 tab_session_id: self.inner.session_id.clone(),
796 command: Some(TabCommand::FocusElement(FocusElementCommand {
797 css_selector: css_selector.into(),
798 retry_options: command_retry_options(options.timeout_ms),
799 })),
800 })
801 .await
802 .map_err(|_| Error::new("failed to send FocusElementCommand"))?;
803 loop {
804 let event =
805 handle.events.message().await?.ok_or_else(|| {
806 Error::new("tab session closed while waiting for focus result")
807 })?;
808 match event.event {
809 Some(TabEvent::Attached(_)) => {}
810 Some(TabEvent::ElementFocused(focused)) => {
811 return Ok(ElementResult {
812 selector: focused.css_selector,
813 note: focused.note,
814 });
815 }
816 Some(TabEvent::Error(error)) => {
817 return Err(Error::new(format!(
818 "tab session error while focusing: {}",
819 error.message
820 )));
821 }
822 Some(TabEvent::Closed(_)) => {
823 handle.closed = true;
824 return Err(Error::new(format!(
825 "tab session {} closed while waiting for focus result",
826 self.inner.session_id
827 )));
828 }
829 _ => {}
830 }
831 }
832 }
833
834 pub async fn fill(
835 &self,
836 css_selector: impl Into<String>,
837 value: impl Into<String>,
838 ) -> Result<FillResult> {
839 self.fill_with_options(css_selector, value, CommandOptions::default())
840 .await
841 }
842
843 pub async fn fill_with_options(
844 &self,
845 css_selector: impl Into<String>,
846 value: impl Into<String>,
847 options: CommandOptions,
848 ) -> Result<FillResult> {
849 let mut state = self.inner.state.lock().await;
850 let handle = self.ensure_handle(&mut state).await?;
851 if handle.closed {
852 return Err(Error::new(format!(
853 "tab session {} is closed",
854 self.inner.session_id
855 )));
856 }
857 handle
858 .command_tx
859 .send(TabSessionCommand {
860 browser_session_id: self.inner.browser_session_id.clone(),
861 tab_session_id: self.inner.session_id.clone(),
862 command: Some(TabCommand::FillElement(FillElementCommand {
863 css_selector: css_selector.into(),
864 value: value.into(),
865 retry_options: command_retry_options(options.timeout_ms),
866 })),
867 })
868 .await
869 .map_err(|_| Error::new("failed to send FillElementCommand"))?;
870 loop {
871 let event =
872 handle.events.message().await?.ok_or_else(|| {
873 Error::new("tab session closed while waiting for fill result")
874 })?;
875 match event.event {
876 Some(TabEvent::Attached(_)) => {}
877 Some(TabEvent::ElementFilled(filled)) => {
878 return Ok(FillResult {
879 selector: filled.css_selector,
880 value: filled.value,
881 note: filled.note,
882 });
883 }
884 Some(TabEvent::Error(error)) => {
885 return Err(Error::new(format!(
886 "tab session error while filling: {}",
887 error.message
888 )));
889 }
890 Some(TabEvent::Closed(_)) => {
891 handle.closed = true;
892 return Err(Error::new(format!(
893 "tab session {} closed while waiting for fill result",
894 self.inner.session_id
895 )));
896 }
897 _ => {}
898 }
899 }
900 }
901
902 pub async fn hover(&self, css_selector: impl Into<String>) -> Result<ElementResult> {
903 self.hover_with_options(css_selector, CommandOptions::default())
904 .await
905 }
906
907 pub async fn hover_with_options(
908 &self,
909 css_selector: impl Into<String>,
910 options: CommandOptions,
911 ) -> Result<ElementResult> {
912 let mut state = self.inner.state.lock().await;
913 let handle = self.ensure_handle(&mut state).await?;
914 if handle.closed {
915 return Err(Error::new(format!(
916 "tab session {} is closed",
917 self.inner.session_id
918 )));
919 }
920 handle
921 .command_tx
922 .send(TabSessionCommand {
923 browser_session_id: self.inner.browser_session_id.clone(),
924 tab_session_id: self.inner.session_id.clone(),
925 command: Some(TabCommand::HoverElement(HoverElementCommand {
926 css_selector: css_selector.into(),
927 retry_options: command_retry_options(options.timeout_ms),
928 })),
929 })
930 .await
931 .map_err(|_| Error::new("failed to send HoverElementCommand"))?;
932 loop {
933 let event =
934 handle.events.message().await?.ok_or_else(|| {
935 Error::new("tab session closed while waiting for hover result")
936 })?;
937 match event.event {
938 Some(TabEvent::Attached(_)) => {}
939 Some(TabEvent::ElementHovered(hovered)) => {
940 return Ok(ElementResult {
941 selector: hovered.css_selector,
942 note: hovered.note,
943 });
944 }
945 Some(TabEvent::Error(error)) => {
946 return Err(Error::new(format!(
947 "tab session error while hovering: {}",
948 error.message
949 )));
950 }
951 Some(TabEvent::Closed(_)) => {
952 handle.closed = true;
953 return Err(Error::new(format!(
954 "tab session {} closed while waiting for hover result",
955 self.inner.session_id
956 )));
957 }
958 _ => {}
959 }
960 }
961 }
962
963 pub async fn press(
964 &self,
965 css_selector: impl Into<String>,
966 key: impl Into<String>,
967 ) -> Result<PressResult> {
968 self.press_with_options(css_selector, key, PressOptions::default())
969 .await
970 }
971
972 pub async fn press_with_options(
973 &self,
974 css_selector: impl Into<String>,
975 key: impl Into<String>,
976 options: PressOptions,
977 ) -> Result<PressResult> {
978 let mut state = self.inner.state.lock().await;
979 let handle = self.ensure_handle(&mut state).await?;
980 if handle.closed {
981 return Err(Error::new(format!(
982 "tab session {} is closed",
983 self.inner.session_id
984 )));
985 }
986 handle
987 .command_tx
988 .send(TabSessionCommand {
989 browser_session_id: self.inner.browser_session_id.clone(),
990 tab_session_id: self.inner.session_id.clone(),
991 command: Some(TabCommand::PressKey(PressKeyCommand {
992 css_selector: css_selector.into(),
993 key: key.into(),
994 text: options.text,
995 retry_options: command_retry_options(options.timeout_ms),
996 })),
997 })
998 .await
999 .map_err(|_| Error::new("failed to send PressKeyCommand"))?;
1000 loop {
1001 let event =
1002 handle.events.message().await?.ok_or_else(|| {
1003 Error::new("tab session closed while waiting for press result")
1004 })?;
1005 match event.event {
1006 Some(TabEvent::Attached(_)) => {}
1007 Some(TabEvent::KeyPressed(pressed)) => {
1008 return Ok(PressResult {
1009 selector: pressed.css_selector,
1010 key: pressed.key,
1011 note: pressed.note,
1012 });
1013 }
1014 Some(TabEvent::Error(error)) => {
1015 return Err(Error::new(format!(
1016 "tab session error while pressing key: {}",
1017 error.message
1018 )));
1019 }
1020 Some(TabEvent::Closed(_)) => {
1021 handle.closed = true;
1022 return Err(Error::new(format!(
1023 "tab session {} closed while waiting for press result",
1024 self.inner.session_id
1025 )));
1026 }
1027 _ => {}
1028 }
1029 }
1030 }
1031
1032 pub async fn text_content(&self, css_selector: impl Into<String>) -> Result<TextResult> {
1033 self.text_content_with_options(css_selector, CommandOptions::default())
1034 .await
1035 }
1036
1037 pub async fn text_content_with_options(
1038 &self,
1039 css_selector: impl Into<String>,
1040 options: CommandOptions,
1041 ) -> Result<TextResult> {
1042 self.read_text(css_selector.into(), options, true).await
1043 }
1044
1045 pub async fn inner_text(&self, css_selector: impl Into<String>) -> Result<TextResult> {
1046 self.inner_text_with_options(css_selector, CommandOptions::default())
1047 .await
1048 }
1049
1050 pub async fn inner_text_with_options(
1051 &self,
1052 css_selector: impl Into<String>,
1053 options: CommandOptions,
1054 ) -> Result<TextResult> {
1055 self.read_text(css_selector.into(), options, false).await
1056 }
1057
1058 pub async fn wait_for_selector(
1059 &self,
1060 css_selector: impl Into<String>,
1061 ) -> Result<WaitForSelectorResult> {
1062 self.wait_for_selector_with_options(css_selector, WaitForSelectorOptions::default())
1063 .await
1064 }
1065
1066 pub async fn wait_for_selector_with_options(
1067 &self,
1068 css_selector: impl Into<String>,
1069 options: WaitForSelectorOptions,
1070 ) -> Result<WaitForSelectorResult> {
1071 let mut state = self.inner.state.lock().await;
1072 let handle = self.ensure_handle(&mut state).await?;
1073 if handle.closed {
1074 return Err(Error::new(format!(
1075 "tab session {} is closed",
1076 self.inner.session_id
1077 )));
1078 }
1079 handle
1080 .command_tx
1081 .send(TabSessionCommand {
1082 browser_session_id: self.inner.browser_session_id.clone(),
1083 tab_session_id: self.inner.session_id.clone(),
1084 command: Some(TabCommand::WaitForSelector(WaitForSelectorCommand {
1085 css_selector: css_selector.into(),
1086 visible: options.visible,
1087 retry_options: command_retry_options(options.timeout_ms),
1088 })),
1089 })
1090 .await
1091 .map_err(|_| Error::new("failed to send WaitForSelectorCommand"))?;
1092 loop {
1093 let event = handle
1094 .events
1095 .message()
1096 .await?
1097 .ok_or_else(|| Error::new("tab session closed while waiting for selector"))?;
1098 match event.event {
1099 Some(TabEvent::Attached(_)) => {}
1100 Some(TabEvent::SelectorWaitSatisfied(waited)) => {
1101 return Ok(WaitForSelectorResult {
1102 selector: waited.css_selector,
1103 visible: waited.visible,
1104 note: waited.note,
1105 });
1106 }
1107 Some(TabEvent::Error(error)) => {
1108 return Err(Error::new(format!(
1109 "tab session error while waiting for selector: {}",
1110 error.message
1111 )));
1112 }
1113 Some(TabEvent::Closed(_)) => {
1114 handle.closed = true;
1115 return Err(Error::new(format!(
1116 "tab session {} closed while waiting for selector result",
1117 self.inner.session_id
1118 )));
1119 }
1120 _ => {}
1121 }
1122 }
1123 }
1124
1125 async fn read_text(
1126 &self,
1127 css_selector: String,
1128 options: CommandOptions,
1129 text_content: bool,
1130 ) -> Result<TextResult> {
1131 let mut state = self.inner.state.lock().await;
1132 let handle = self.ensure_handle(&mut state).await?;
1133 if handle.closed {
1134 return Err(Error::new(format!(
1135 "tab session {} is closed",
1136 self.inner.session_id
1137 )));
1138 }
1139 let command = if text_content {
1140 TabCommand::GetTextContent(GetTextContentCommand {
1141 css_selector,
1142 retry_options: command_retry_options(options.timeout_ms),
1143 })
1144 } else {
1145 TabCommand::GetInnerText(GetInnerTextCommand {
1146 css_selector,
1147 retry_options: command_retry_options(options.timeout_ms),
1148 })
1149 };
1150 handle
1151 .command_tx
1152 .send(TabSessionCommand {
1153 browser_session_id: self.inner.browser_session_id.clone(),
1154 tab_session_id: self.inner.session_id.clone(),
1155 command: Some(command),
1156 })
1157 .await
1158 .map_err(|_| Error::new("failed to send text command"))?;
1159 loop {
1160 let event =
1161 handle.events.message().await?.ok_or_else(|| {
1162 Error::new("tab session closed while waiting for text result")
1163 })?;
1164 match event.event {
1165 Some(TabEvent::Attached(_)) => {}
1166 Some(TabEvent::TextContentResolved(text)) => {
1167 return Ok(TextResult {
1168 selector: text.css_selector,
1169 text: text.text,
1170 note: text.note,
1171 });
1172 }
1173 Some(TabEvent::InnerTextResolved(text)) => {
1174 return Ok(TextResult {
1175 selector: text.css_selector,
1176 text: text.text,
1177 note: text.note,
1178 });
1179 }
1180 Some(TabEvent::Error(error)) => {
1181 return Err(Error::new(format!(
1182 "tab session error while reading text: {}",
1183 error.message
1184 )));
1185 }
1186 Some(TabEvent::Closed(_)) => {
1187 handle.closed = true;
1188 return Err(Error::new(format!(
1189 "tab session {} closed while waiting for text result",
1190 self.inner.session_id
1191 )));
1192 }
1193 _ => {}
1194 }
1195 }
1196 }
1197
1198 pub async fn close(&self) -> Result<()> {
1199 let mut state = self.inner.state.lock().await;
1200 let handle = self.ensure_handle(&mut state).await?;
1201 if handle.closed {
1202 return Ok(());
1203 }
1204
1205 handle
1206 .command_tx
1207 .send(TabSessionCommand {
1208 browser_session_id: self.inner.browser_session_id.clone(),
1209 tab_session_id: self.inner.session_id.clone(),
1210 command: Some(TabCommand::Close(CloseTabSessionCommand {})),
1211 })
1212 .await
1213 .map_err(|_| Error::new("failed to send CloseTabSessionCommand"))?;
1214
1215 loop {
1216 let event = handle
1217 .events
1218 .message()
1219 .await?
1220 .ok_or_else(|| Error::new("tab session closed before close confirmation"))?;
1221
1222 match event.event {
1223 Some(TabEvent::Attached(_)) => {}
1224 Some(TabEvent::Closed(_)) => {
1225 handle.closed = true;
1226 return Ok(());
1227 }
1228 Some(TabEvent::Error(error)) => {
1229 return Err(Error::new(format!(
1230 "tab session error while closing: {}",
1231 error.message
1232 )));
1233 }
1234 _ => {}
1235 }
1236 }
1237 }
1238
1239 async fn ensure_handle<'a>(&self, state: &'a mut TabState) -> Result<&'a mut TabHandle> {
1240 if state.handle.is_none() {
1241 let mut engine = self.inner.runtime.engine.clone();
1242 let (command_tx, command_rx) = mpsc::channel(16);
1243 let response = engine
1244 .tab_session(tonic::Request::new(ReceiverStream::new(command_rx)))
1245 .await?;
1246 state.handle = Some(TabHandle {
1247 command_tx,
1248 events: response.into_inner(),
1249 closed: false,
1250 });
1251 }
1252
1253 state
1254 .handle
1255 .as_mut()
1256 .ok_or_else(|| Error::new("tab session handle was not initialized"))
1257 }
1258}
1259
1260async fn get_runtime() -> Result<Arc<RuntimeClient>> {
1261 if let Ok(runtime) = runtime_slot().lock() {
1262 if let Some(existing) = runtime.as_ref() {
1263 return Ok(Arc::clone(existing));
1264 }
1265 }
1266
1267 let endpoint = configured_server_addr();
1268 let engine = EngineServiceClient::connect(endpoint).await?;
1269 let runtime = Arc::new(RuntimeClient { engine });
1270
1271 let mut slot = runtime_slot()
1272 .lock()
1273 .map_err(|_| Error::new("runtime singleton lock is poisoned"))?;
1274 if let Some(existing) = slot.as_ref() {
1275 return Ok(Arc::clone(existing));
1276 }
1277 *slot = Some(Arc::clone(&runtime));
1278 Ok(runtime)
1279}
1280
1281fn runtime_slot() -> &'static Mutex<Option<Arc<RuntimeClient>>> {
1282 RUNTIME.get_or_init(|| Mutex::new(None))
1283}
1284
1285fn server_addr_override_slot() -> &'static Mutex<Option<String>> {
1286 SERVER_ADDR_OVERRIDE.get_or_init(|| Mutex::new(None))
1287}
1288
1289fn configured_server_addr() -> String {
1290 if let Ok(server_addr_override) = server_addr_override_slot().lock() {
1291 if let Some(server_addr) = server_addr_override.as_ref() {
1292 return server_addr.clone();
1293 }
1294 }
1295
1296 normalize_server_addr(
1297 std::env::var(SERVER_ADDR_ENV_VAR)
1298 .ok()
1299 .filter(|value| !value.trim().is_empty())
1300 .as_deref()
1301 .unwrap_or(DEFAULT_SERVER_ADDR),
1302 )
1303}
1304
1305fn normalize_server_addr(raw: &str) -> String {
1306 let trimmed = raw.trim();
1307 if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
1308 trimmed.to_string()
1309 } else {
1310 format!("http://{trimmed}")
1311 }
1312}
1313
1314fn command_retry_options(timeout_ms: Option<u32>) -> Option<CommandRetryOptions> {
1315 timeout_ms.map(|timeout_ms| CommandRetryOptions {
1316 timeout_ms: Some(timeout_ms),
1317 retry_interval_ms: None,
1318 })
1319}
1320
1321fn count_result_from_event(event: ElementCountedEvent) -> CountResult {
1322 CountResult {
1323 selector: event.css_selector,
1324 count: event.count,
1325 note: event.note,
1326 }
1327}
1328
1329fn highlight_result_from_event(event: ElementsHighlightedEvent) -> HighlightResult {
1330 HighlightResult {
1331 selector: event.css_selector,
1332 count: event.count,
1333 note: event.note,
1334 }
1335}