Skip to main content

allwright/
client.rs

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}