Skip to main content

sl4f_lib/webdriver/
facade.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::webdriver::types::{EnableDevToolsResult, GetDevToolsPortsResult};
6use anyhow::{Error, format_err};
7use fidl::endpoints::{ServerEnd, create_request_stream};
8use fidl_fuchsia_web::{
9    DevToolsListenerMarker, DevToolsListenerRequest, DevToolsListenerRequestStream,
10    DevToolsPerContextListenerMarker, DevToolsPerContextListenerRequest,
11    DevToolsPerContextListenerRequestStream,
12};
13use fuchsia_async as fasync;
14use fuchsia_sync::Mutex;
15use futures::channel::mpsc;
16use futures::prelude::*;
17use log::*;
18use std::collections::HashSet;
19use std::ops::DerefMut;
20
21/// Facade providing access to WebDriver debug services.  Supports enabling
22/// DevTools ports on Chrome contexts, and retrieving the set of open ports.
23/// Open ports can be used by Chromedriver to manipulate contexts for testing.
24#[derive(Debug)]
25pub struct WebdriverFacade {
26    /// Internal facade, instantiated when facade is initialized with
27    /// `enable_dev_tools`.
28    internal: Mutex<Option<WebdriverFacadeInternal>>,
29}
30
31impl WebdriverFacade {
32    /// Create a new `WebdriverFacade`
33    pub fn new() -> WebdriverFacade {
34        WebdriverFacade { internal: Mutex::new(None) }
35    }
36
37    /// Configure WebDriver to start any future contexts in debug mode.  This
38    /// allows contexts to be controlled remotely through ChromeDriver.
39    pub async fn enable_dev_tools(&self) -> Result<EnableDevToolsResult, Error> {
40        if self.internal.lock().is_some() {
41            return Err(format_err!("DevTools already enabled."));
42        }
43        let initialized_internal = WebdriverFacadeInternal::new().await?;
44
45        let mut internal = self.internal.lock();
46
47        if internal.is_none() {
48            *internal = Some(initialized_internal);
49            Ok(EnableDevToolsResult::Success)
50        } else {
51            Err(format_err!("DevTools already enabled."))
52        }
53    }
54
55    /// Returns a list of open DevTools ports.  Returns an error if DevTools
56    /// have not been enabled using `enable_dev_tools`.
57    pub async fn get_dev_tools_ports(&self) -> Result<GetDevToolsPortsResult, Error> {
58        let mut internal = self.internal.lock();
59        match internal.deref_mut() {
60            Some(facade) => Ok(GetDevToolsPortsResult::new(facade.get_ports())),
61            None => Err(format_err!("DevTools are not enabled.")),
62        }
63    }
64}
65
66/// Internal struct providing updated list of open DevTools ports using
67/// WebDriver Debug service.
68#[derive(Debug)]
69struct WebdriverFacadeInternal {
70    /// Set of currently open DevTools ports.
71    dev_tools_ports: HashSet<u16>,
72    /// Receiving end for port update channel.
73    port_update_receiver: mpsc::UnboundedReceiver<PortUpdateMessage>,
74}
75
76impl WebdriverFacadeInternal {
77    /// Create a new `WebdriverFacadeInternal`.  Can fail if connecting to the
78    /// debug service fails.
79    pub async fn new() -> Result<WebdriverFacadeInternal, Error> {
80        let port_update_receiver = Self::get_port_event_receiver().await?;
81        Ok(WebdriverFacadeInternal { dev_tools_ports: HashSet::new(), port_update_receiver })
82    }
83
84    /// Returns a copy of the available ports.
85    pub fn get_ports(&mut self) -> Vec<u16> {
86        self.update_port_set();
87        Vec::from_iter(self.dev_tools_ports.iter().cloned())
88    }
89
90    /// Consumes messages produced by context listeners to update set of open ports.
91    fn update_port_set(&mut self) {
92        while let Ok(update) = self.port_update_receiver.try_recv() {
93            match update {
94                PortUpdateMessage::PortOpened(port) => self.dev_tools_ports.insert(port),
95                PortUpdateMessage::PortClosed(port) => self.dev_tools_ports.remove(&port),
96            };
97        }
98    }
99
100    /// Setup a channel to receive port open/close channels and return the
101    /// receiving end.  Assumes Webdriver is already running.
102    async fn get_port_event_receiver() -> Result<mpsc::UnboundedReceiver<PortUpdateMessage>, Error>
103    {
104        let (port_update_sender, port_update_receiver) = mpsc::unbounded();
105
106        let debug = Self::spawn_dev_tools_listener_at_path(
107            "/svc/fuchsia.web.Debug",
108            port_update_sender.clone(),
109        );
110        let debug_context_provider = Self::spawn_dev_tools_listener_at_path(
111            "/svc/fuchsia.web.Debug-context_provider",
112            port_update_sender,
113        );
114
115        // Wait for initializing the two providers to complete, and continue so long as one or other is functional.
116        let debug_result = debug.await;
117        let debug_context_provider_result = debug_context_provider.await;
118        debug_result.or(debug_context_provider_result)?;
119
120        Ok(port_update_receiver)
121    }
122
123    /// Spawn an instance of `DevToolsListener` that forwards port open/close
124    /// events from the debug request channel at `protocol_path` to the supplied
125    /// mpsc channel.
126    async fn spawn_dev_tools_listener_at_path(
127        protocol_path: &str,
128        port_update_sender: mpsc::UnboundedSender<PortUpdateMessage>,
129    ) -> Result<(), Error> {
130        // Connect to the specified protocol path.
131        let debug_proxy = fuchsia_component::client::connect_to_protocol_at_path::<
132            fidl_fuchsia_web::DebugMarker,
133        >(protocol_path)?;
134
135        // Create a DevToolsListener and channel, and enable DevTools.
136        let (dev_tools_client, dev_tools_stream) =
137            create_request_stream::<DevToolsListenerMarker>();
138        debug_proxy.enable_dev_tools(dev_tools_client).await?;
139
140        // Spawn a task to process the DevToolsListener updates asynchronously.
141        fasync::Task::spawn(async move {
142            let dev_tools_listener = DevToolsListener::new(port_update_sender);
143            dev_tools_listener
144                .handle_requests_from_stream(dev_tools_stream)
145                .await
146                .unwrap_or_else(|_| print!("Error handling DevToolsListener channel!"));
147        })
148        .detach();
149
150        Ok(())
151    }
152}
153
154/// Message passed from a `DevToolsPerContextListener` to
155/// `WebdriverFacade` to notify it of a port opening or closing
156#[derive(Debug)]
157enum PortUpdateMessage {
158    /// Sent when a port is opened.
159    PortOpened(u16),
160    /// Sent when a port is closed.
161    PortClosed(u16),
162}
163
164/// An implementation of `fuchsia.web.DevToolsListener` that instantiates
165/// `DevToolsPerContextListener` when a context is created.
166struct DevToolsListener {
167    /// Sender end of port update channel.
168    port_update_sender: mpsc::UnboundedSender<PortUpdateMessage>,
169}
170
171impl DevToolsListener {
172    /// Create a new `DevToolsListener`
173    fn new(port_update_sender: mpsc::UnboundedSender<PortUpdateMessage>) -> Self {
174        DevToolsListener { port_update_sender }
175    }
176
177    /// Handle requests made to `DevToolsListener`.
178    pub async fn handle_requests_from_stream(
179        &self,
180        mut stream: DevToolsListenerRequestStream,
181    ) -> Result<(), Error> {
182        while let Some(request) = stream.try_next().await? {
183            let DevToolsListenerRequest::OnContextDevToolsAvailable { listener, .. } = request;
184            self.on_context_created(listener)?;
185        }
186        Ok(())
187    }
188
189    /// Handles OnContextDevToolsAvailable.  Spawns an instance of
190    /// `DevToolsPerContextListener` to handle the new Chrome context.
191    fn on_context_created(
192        &self,
193        listener: ServerEnd<DevToolsPerContextListenerMarker>,
194    ) -> Result<(), Error> {
195        info!("Chrome context created");
196        let listener_request_stream = listener.into_stream();
197        let port_update_sender = mpsc::UnboundedSender::clone(&self.port_update_sender);
198        fasync::Task::spawn(async move {
199            let mut per_context_listener = DevToolsPerContextListener::new(port_update_sender);
200            per_context_listener
201                .handle_requests_from_stream(listener_request_stream)
202                .await
203                .unwrap_or_else(|_| warn!("Error handling DevToolsListener channel!"));
204        })
205        .detach();
206        Ok(())
207    }
208}
209
210/// An implementation of `fuchsia.web.DevToolsPerContextListener` that forwards
211/// port open/close events to an mpsc channel.
212struct DevToolsPerContextListener {
213    /// Sender end of port update channel.
214    port_update_sender: mpsc::UnboundedSender<PortUpdateMessage>,
215}
216
217impl DevToolsPerContextListener {
218    /// Create a new `DevToolsPerContextListener`
219    fn new(port_update_sender: mpsc::UnboundedSender<PortUpdateMessage>) -> Self {
220        DevToolsPerContextListener { port_update_sender }
221    }
222
223    /// Handle requests made to `DevToolsPerContextListener`.  The HTTP port
224    /// becomes available when OnHttpPortOpen is called, and becomes
225    /// unavailable when the stream closes.
226    pub async fn handle_requests_from_stream(
227        &mut self,
228        mut stream: DevToolsPerContextListenerRequestStream,
229    ) -> Result<(), Error> {
230        let mut context_port = None;
231
232        while let Ok(Some(request)) = stream.try_next().await {
233            let DevToolsPerContextListenerRequest::OnHttpPortOpen { port, .. } = request;
234            context_port.replace(port);
235            self.on_port_open(port)?;
236        }
237
238        // Port is closed after stream ends.
239        if let Some(port) = context_port {
240            self.on_port_closed(port)?;
241        }
242        Ok(())
243    }
244
245    /// Send a port open event.
246    fn on_port_open(&mut self, port: u16) -> Result<(), Error> {
247        info!("DevTools port {:?} opened", port);
248        self.port_update_sender
249            .unbounded_send(PortUpdateMessage::PortOpened(port))
250            .map_err(|_| format_err!("Error sending port open message"))
251    }
252
253    /// Send a port close event.
254    fn on_port_closed(&mut self, port: u16) -> Result<(), Error> {
255        info!("DevTools port {:?} closed", port);
256        self.port_update_sender
257            .unbounded_send(PortUpdateMessage::PortClosed(port))
258            .map_err(|_| format_err!("Error sending port closed message"))
259    }
260}