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