diff options
Diffstat (limited to 'src/marionette.rs')
-rw-r--r-- | src/marionette.rs | 713 |
1 files changed, 239 insertions, 474 deletions
diff --git a/src/marionette.rs b/src/marionette.rs index 87657ba..cf3f40e 100644 --- a/src/marionette.rs +++ b/src/marionette.rs @@ -2,11 +2,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::android::AndroidHandler; +use crate::browser::{Browser, LocalBrowser, RemoteBrowser}; +use crate::build; +use crate::capabilities::{FirefoxCapabilities, FirefoxOptions}; use crate::command::{ AddonInstallParameters, AddonUninstallParameters, GeckoContextParameters, GeckoExtensionCommand, GeckoExtensionRoute, CHROME_ELEMENT_KEY, }; +use crate::logging; use marionette_rs::common::{ Cookie as MarionetteCookie, Date as MarionetteDate, Frame as MarionetteFrame, Timeouts as MarionetteTimeouts, WebElement as MarionetteWebElement, Window, @@ -22,9 +25,6 @@ use marionette_rs::webdriver::{ Url as MarionetteUrl, WindowRect as MarionetteWindowRect, }; use mozdevice::AndroidStorageInput; -use mozprofile::preferences::Pref; -use mozprofile::profile::Profile; -use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess}; use serde::de::{self, Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use serde_json::{self, Map, Value}; @@ -32,12 +32,11 @@ use std::io::prelude::*; use std::io::Error as IoError; use std::io::ErrorKind; use std::io::Result as IoResult; -use std::net::{TcpListener, TcpStream}; +use std::net::{Shutdown, TcpListener, TcpStream}; use std::path::PathBuf; use std::sync::Mutex; use std::thread; use std::time; -use webdriver::capabilities::CapabilitiesMatching; use webdriver::command::WebDriverCommand::{ AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert, ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension, @@ -66,24 +65,10 @@ use webdriver::response::{ NewWindowResponse, TimeoutsResponse, ValueResponse, WebDriverResponse, WindowRectResponse, }; use webdriver::server::{Session, WebDriverHandler}; - -use crate::build; -use crate::capabilities::{FirefoxCapabilities, FirefoxOptions}; -use crate::logging; -use crate::prefs; - -/// A running Gecko instance. -#[derive(Debug)] -pub enum Browser { - /// A local Firefox process, running on this (host) device. - Host(FirefoxProcess), - - /// A remote instance, running on a (target) Android device. - Target(AndroidHandler), -} +use webdriver::{capabilities::CapabilitiesMatching, server::SessionTeardownKind}; #[derive(Debug, PartialEq, Deserialize)] -pub struct MarionetteHandshake { +struct MarionetteHandshake { #[serde(rename = "marionetteProtocol")] protocol: u16, #[serde(rename = "applicationType")] @@ -91,41 +76,40 @@ pub struct MarionetteHandshake { } #[derive(Default)] -pub struct MarionetteSettings { - pub host: String, - pub port: Option<u16>, - pub binary: Option<PathBuf>, - pub connect_existing: bool, +pub(crate) struct MarionetteSettings { + pub(crate) binary: Option<PathBuf>, + pub(crate) connect_existing: bool, + pub(crate) host: String, + pub(crate) port: Option<u16>, + pub(crate) websocket_port: u16, /// Brings up the Browser Toolbox when starting Firefox, /// letting you debug internals. - pub jsdebugger: bool, + pub(crate) jsdebugger: bool, - pub android_storage: AndroidStorageInput, + pub(crate) android_storage: AndroidStorageInput, } #[derive(Default)] -pub struct MarionetteHandler { - pub connection: Mutex<Option<MarionetteConnection>>, - pub settings: MarionetteSettings, - pub browser: Option<Browser>, +pub(crate) struct MarionetteHandler { + connection: Mutex<Option<MarionetteConnection>>, + settings: MarionetteSettings, } impl MarionetteHandler { - pub fn new(settings: MarionetteSettings) -> MarionetteHandler { + pub(crate) fn new(settings: MarionetteSettings) -> MarionetteHandler { MarionetteHandler { connection: Mutex::new(None), settings, - browser: None, } } - pub fn create_connection( - &mut self, - session_id: &Option<String>, + fn create_connection( + &self, + session_id: Option<String>, new_session_parameters: &NewSessionParameters, - ) -> WebDriverResult<Map<String, Value>> { - let (options, capabilities) = { + ) -> WebDriverResult<MarionetteConnection> { + let (capabilities, options) = { let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref()); let mut capabilities = new_session_parameters .match_browser(&mut fx_capabilities)? @@ -138,188 +122,64 @@ impl MarionetteHandler { let options = FirefoxOptions::from_capabilities( fx_capabilities.chosen_binary, - self.settings.android_storage, + &self.settings, &mut capabilities, )?; - (options, capabilities) + (capabilities, options) }; if let Some(l) = options.log.level { logging::set_max_level(l); } - let host = self.settings.host.to_owned(); - let port = self.settings.port.unwrap_or(get_free_port(&host)?); - - match options.android { - Some(_) => { - // TODO: support connecting to running Apps. There's no real obstruction here, - // just some details about port forwarding to work through. We can't follow - // `chromedriver` here since it uses an abstract socket rather than a TCP socket: - // see bug 1240830 for thoughts on doing that for Marionette. - if self.settings.connect_existing { - return Err(WebDriverError::new( - ErrorStatus::SessionNotCreated, - "Cannot connect to an existing Android App yet", - )); - } - - self.start_android(port, options)?; - } - None => { - if !self.settings.connect_existing { - self.start_browser(port, options)?; - } - } - } - - let mut connection = MarionetteConnection::new(host, port, session_id.clone()); - connection.connect(&mut self.browser).or_else(|e| { - match self.browser { - Some(Browser::Host(ref mut runner)) => { - runner.kill()?; - } - Some(Browser::Target(ref mut handler)) => { - handler.force_stop().map_err(|e| { - WebDriverError::new(ErrorStatus::UnknownError, e.to_string()) - })?; - } - _ => {} - } - - Err(e) - })?; - self.connection = Mutex::new(Some(connection)); - Ok(capabilities) - } - - fn start_android(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> { - let android_options = options.android.unwrap(); - - let handler = AndroidHandler::new(&android_options, port)?; - - // Profile management. - let is_custom_profile = options.profile.is_some(); - - let mut profile = options.profile.unwrap_or(Profile::new()?); - - self.set_prefs( - handler.target_port, - &mut profile, - is_custom_profile, - options.prefs, - ) - .map_err(|e| { - WebDriverError::new( - ErrorStatus::SessionNotCreated, - format!("Failed to set preferences: {}", e), - ) - })?; - - handler - .prepare(&profile, options.env.unwrap_or_default()) - .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?; - - handler - .launch() - .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?; - - self.browser = Some(Browser::Target(handler)); + let marionette_host = self.settings.host.to_owned(); + let marionette_port = self + .settings + .port + .unwrap_or(get_free_port(&marionette_host)?); - Ok(()) - } - - fn start_browser(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> { - let binary = options.binary.ok_or_else(|| { - WebDriverError::new( - ErrorStatus::SessionNotCreated, - "Expected browser binary location, but unable to find \ - binary in default location, no \ - 'moz:firefoxOptions.binary' capability provided, and \ - no binary flag set on the command line", - ) - })?; - - let is_custom_profile = options.profile.is_some(); - - let mut profile = match options.profile { - Some(x) => x, - None => Profile::new()?, + let websocket_port = match options.use_websocket { + true => Some(self.settings.websocket_port), + false => None, }; - self.set_prefs(port, &mut profile, is_custom_profile, options.prefs) - .map_err(|e| { - WebDriverError::new( + let browser = if options.android.is_some() { + // TODO: support connecting to running Apps. There's no real obstruction here, + // just some details about port forwarding to work through. We can't follow + // `chromedriver` here since it uses an abstract socket rather than a TCP socket: + // see bug 1240830 for thoughts on doing that for Marionette. + if self.settings.connect_existing { + return Err(WebDriverError::new( ErrorStatus::SessionNotCreated, - format!("Failed to set preferences: {}", e), - ) - })?; - - let mut runner = FirefoxRunner::new(&binary, profile); - - runner.arg("--marionette"); - if self.settings.jsdebugger { - runner.arg("--jsdebugger"); - } - if let Some(args) = options.args.as_ref() { - runner.args(args); - } - - // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting - runner - .env("MOZ_CRASHREPORTER", "1") - .env("MOZ_CRASHREPORTER_NO_REPORT", "1") - .env("MOZ_CRASHREPORTER_SHUTDOWN", "1"); - - let browser_proc = runner.start().map_err(|e| { - WebDriverError::new( - ErrorStatus::SessionNotCreated, - format!("Failed to start browser {}: {}", binary.display(), e), - ) - })?; - self.browser = Some(Browser::Host(browser_proc)); - - Ok(()) + "Cannot connect to an existing Android App yet", + )); + } + Browser::Remote(RemoteBrowser::new( + options, + marionette_port, + websocket_port, + )?) + } else if !self.settings.connect_existing { + Browser::Local(LocalBrowser::new( + options, + marionette_port, + self.settings.jsdebugger, + )?) + } else { + Browser::Existing + }; + let session = MarionetteSession::new(session_id, capabilities); + MarionetteConnection::new(marionette_host, marionette_port, browser, session) } - pub fn set_prefs( - &self, - port: u16, - profile: &mut Profile, - custom_profile: bool, - extra_prefs: Vec<(String, Pref)>, - ) -> WebDriverResult<()> { - let prefs = profile.user_prefs().map_err(|_| { - WebDriverError::new( - ErrorStatus::UnknownError, - "Unable to read profile preferences file", - ) - })?; - - for &(ref name, ref value) in prefs::DEFAULT.iter() { - if !custom_profile || !prefs.contains_key(name) { - prefs.insert((*name).to_string(), (*value).clone()); + fn close_connection(&mut self, wait_for_shutdown: bool) { + if let Ok(connection) = self.connection.get_mut() { + if let Some(conn) = connection.take() { + if let Err(e) = conn.close(wait_for_shutdown) { + error!("Failed to close browser connection: {}", e) + } } } - - prefs.insert_slice(&extra_prefs[..]); - - if self.settings.jsdebugger { - prefs.insert("devtools.browsertoolbox.panel", Pref::new("jsdebugger")); - prefs.insert("devtools.debugger.remote-enabled", Pref::new(true)); - prefs.insert("devtools.chrome.enabled", Pref::new(true)); - prefs.insert("devtools.debugger.prompt-connection", Pref::new(false)); - } - - prefs.insert("marionette.log.level", logging::max_level().into()); - prefs.insert("marionette.port", Pref::new(port)); - - prefs.write().map_err(|e| { - WebDriverError::new( - ErrorStatus::UnknownError, - format!("Unable to write Firefox profile: {}", e), - ) - }) } } @@ -329,75 +189,49 @@ impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler { _: &Option<Session>, msg: WebDriverMessage<GeckoExtensionRoute>, ) -> WebDriverResult<WebDriverResponse> { - let mut resolved_capabilities = None; - { - let mut capabilities_options = None; - // First handle the status message which doesn't actually require a marionette - // connection or message - if let Status = msg.command { - let (ready, message) = self - .connection - .lock() - .map(|ref connection| { - connection - .as_ref() - .map(|_| (false, "Session already started")) - .unwrap_or((true, "")) - }) - .unwrap_or((false, "geckodriver internal error")); - let mut value = Map::new(); - value.insert("ready".to_string(), Value::Bool(ready)); - value.insert("message".to_string(), Value::String(message.into())); - return Ok(WebDriverResponse::Generic(ValueResponse(Value::Object( - value, - )))); - } - - match self.connection.lock() { - Ok(ref connection) => { - if connection.is_none() { - match msg.command { - NewSession(ref capabilities) => { - capabilities_options = Some(capabilities); - } - _ => { - return Err(WebDriverError::new( - ErrorStatus::InvalidSessionId, - "Tried to run command without establishing a connection", - )); - } - } - } - } - Err(_) => { - return Err(WebDriverError::new( - ErrorStatus::UnknownError, - "Failed to aquire Marionette connection", - )) - } - } - if let Some(capabilities) = capabilities_options { - resolved_capabilities = - Some(self.create_connection(&msg.session_id, &capabilities)?); - } + // First handle the status message which doesn't actually require a marionette + // connection or message + if let Status = msg.command { + let (ready, message) = self + .connection + .get_mut() + .map(|ref connection| { + connection + .as_ref() + .map(|_| (false, "Session already started")) + .unwrap_or((true, "")) + }) + .unwrap_or((false, "geckodriver internal error")); + let mut value = Map::new(); + value.insert("ready".to_string(), Value::Bool(ready)); + value.insert("message".to_string(), Value::String(message.into())); + return Ok(WebDriverResponse::Generic(ValueResponse(Value::Object( + value, + )))); } match self.connection.lock() { - Ok(ref mut connection) => { - match connection.as_mut() { - Some(conn) => { - conn.send_command(resolved_capabilities, &msg) - .map_err(|mut err| { - // Shutdown the browser if no session can - // be established due to errors. - if let NewSession(_) = msg.command { - err.delete_session = true; - } - err - }) + Ok(mut connection) => { + if connection.is_none() { + if let NewSession(ref capabilities) = msg.command { + let conn = self.create_connection(msg.session_id.clone(), &capabilities)?; + *connection = Some(conn); + } else { + return Err(WebDriverError::new( + ErrorStatus::InvalidSessionId, + "Tried to run command without establishing a connection", + )); } - None => panic!("Connection missing"), } + let conn = connection.as_mut().expect("Missing connection"); + conn.send_command(&msg).map_err(|mut err| { + // Shutdown the browser if no session can + // be established due to errors. + if let NewSession(_) = msg.command { + err.delete_session = true; + } + err + }) } Err(_) => Err(WebDriverError::new( ErrorStatus::UnknownError, @@ -406,64 +240,38 @@ impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler { } } - fn delete_session(&mut self, session: &Option<Session>) { - if let Some(ref s) = *session { - let delete_session = WebDriverMessage { - session_id: Some(s.id.clone()), - command: WebDriverCommand::DeleteSession, - }; - let _ = self.handle_command(session, delete_session); - } - - if let Ok(ref mut connection) = self.connection.lock() { - if let Some(conn) = connection.as_mut() { - conn.close(); - } - } - - match self.browser { - Some(Browser::Host(ref mut runner)) => { - // TODO(https://bugzil.la/1443922): - // Use toolkit.asyncshutdown.crash_timout pref - match runner.wait(time::Duration::from_secs(70)) { - Ok(x) => debug!("Browser process stopped: {}", x), - Err(e) => error!("Failed to stop browser process: {}", e), - } - } - Some(Browser::Target(ref mut handler)) => { - // Try to force-stop the process on the target device - match handler.force_stop() { - Ok(_) => debug!("Android package force-stopped"), - Err(e) => error!("Failed to force-stop Android package: {}", e), - } - } - None => {} - } + fn teardown_session(&mut self, kind: SessionTeardownKind) { + let wait_for_shutdown = match kind { + SessionTeardownKind::Deleted => true, + SessionTeardownKind::NotDeleted => false, + }; + self.close_connection(wait_for_shutdown); + } +} - self.connection = Mutex::new(None); - self.browser = None; +impl Drop for MarionetteHandler { + fn drop(&mut self) { + self.close_connection(false); } } -pub struct MarionetteSession { - pub session_id: String, - protocol: Option<u16>, - application_type: Option<String>, +struct MarionetteSession { + session_id: String, + capabilities: Map<String, Value>, command_id: MessageId, } impl MarionetteSession { - pub fn new(session_id: Option<String>) -> MarionetteSession { + fn new(session_id: Option<String>, capabilities: Map<String, Value>) -> MarionetteSession { let initital_id = session_id.unwrap_or_else(|| "".to_string()); MarionetteSession { session_id: initital_id, - protocol: None, - application_type: None, + capabilities, command_id: 0, } } - pub fn update( + fn update( &mut self, msg: &WebDriverMessage<GeckoExtensionRoute>, resp: &MarionetteResponse, @@ -479,7 +287,7 @@ impl MarionetteSession { ErrorStatus::SessionNotCreated, "Unable to convert session id to string" ); - self.session_id = session_id.to_string().clone(); + self.session_id = session_id.to_string(); }; Ok(()) } @@ -514,12 +322,12 @@ impl MarionetteSession { Ok(WebElement(id)) } - pub fn next_command_id(&mut self) -> MessageId { + fn next_command_id(&mut self) -> MessageId { self.command_id += 1; self.command_id } - pub fn response( + fn response( &mut self, msg: &WebDriverMessage<GeckoExtensionRoute>, resp: MarionetteResponse, @@ -859,7 +667,7 @@ impl MarionetteSession { WebDriverResponse::NewSession(NewSessionResponse::new( session_id.to_string(), - Value::Object(capabilities.clone()), + Value::Object(capabilities), )) } DeleteSession => WebDriverResponse::DeleteSession, @@ -876,6 +684,7 @@ impl MarionetteSession { fn try_convert_to_marionette_message( msg: &WebDriverMessage<GeckoExtensionRoute>, + browser: &Browser, ) -> WebDriverResult<Option<Command>> { use self::GeckoExtensionCommand::*; use self::WebDriverCommand::*; @@ -892,11 +701,16 @@ fn try_convert_to_marionette_message( DeleteCookies => Some(Command::WebDriver( MarionetteWebDriverCommand::DeleteCookies, )), - DeleteSession => Some(Command::Marionette( - marionette_rs::marionette::Command::DeleteSession { - flags: vec![AppStatus::eForceQuit], - }, - )), + DeleteSession => match browser { + Browser::Local(_) | Browser::Remote(_) => Some(Command::Marionette( + marionette_rs::marionette::Command::DeleteSession { + flags: vec![AppStatus::eForceQuit], + }, + )), + Browser::Existing => Some(Command::WebDriver( + MarionetteWebDriverCommand::DeleteSession, + )), + }, DismissAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::DismissAlert)), ElementClear(ref e) => Some(Command::WebDriver( MarionetteWebDriverCommand::ElementClear(e.to_marionette()?), @@ -910,7 +724,7 @@ fn try_convert_to_marionette_message( MarionetteWebDriverCommand::ElementSendKeys { id: e.clone().to_string(), text: keys.text.clone(), - value: keys.value.clone(), + value: keys.value, }, )) } @@ -932,7 +746,7 @@ fn try_convert_to_marionette_message( MarionetteWebDriverCommand::FindElementElement { element: e.clone().to_string(), using: locator.using.clone(), - value: locator.value.clone(), + value: locator.value, }, )) } @@ -942,7 +756,7 @@ fn try_convert_to_marionette_message( MarionetteWebDriverCommand::FindElementElements { element: e.clone().to_string(), using: locator.using.clone(), - value: locator.value.clone(), + value: locator.value, }, )) } @@ -1068,28 +882,25 @@ fn try_convert_to_marionette_message( MarionetteWebDriverCommand::TakeScreenshot(screenshot), )) } - Extension(ref extension) => match extension { - TakeFullScreenshot => { - let screenshot = ScreenshotOptions { - id: None, - highlights: vec![], - full: true, - }; - Some(Command::WebDriver( - MarionetteWebDriverCommand::TakeFullScreenshot(screenshot), - )) - } - _ => None, - }, + Extension(TakeFullScreenshot) => { + let screenshot = ScreenshotOptions { + id: None, + highlights: vec![], + full: true, + }; + Some(Command::WebDriver( + MarionetteWebDriverCommand::TakeFullScreenshot(screenshot), + )) + } _ => None, }) } #[derive(Debug, PartialEq)] -pub struct MarionetteCommand { - pub id: MessageId, - pub name: String, - pub params: Map<String, Value>, +struct MarionetteCommand { + id: MessageId, + name: String, + params: Map<String, Value>, } impl Serialize for MarionetteCommand { @@ -1118,23 +929,21 @@ impl MarionetteCommand { fn from_webdriver_message( id: MessageId, - capabilities: Option<Map<String, Value>>, + capabilities: &Map<String, Value>, + browser: &Browser, msg: &WebDriverMessage<GeckoExtensionRoute>, ) -> WebDriverResult<String> { use self::GeckoExtensionCommand::*; - if let Some(cmd) = try_convert_to_marionette_message(msg)? { + if let Some(cmd) = try_convert_to_marionette_message(msg, browser)? { let req = Message::Incoming(Request(id, cmd)); MarionetteCommand::encode_msg(req) } else { let (opt_name, opt_parameters) = match msg.command { Status => panic!("Got status command that should already have been handled"), NewSession(_) => { - let caps = capabilities - .expect("Tried to create new session without processing capabilities"); - let mut data = Map::new(); - for (k, v) in caps.iter() { + for (k, v) in capabilities.iter() { data.insert(k.to_string(), serde_json::to_value(v)?); } @@ -1167,10 +976,10 @@ impl MarionetteCommand { } #[derive(Debug, PartialEq)] -pub struct MarionetteResponse { - pub id: MessageId, - pub error: Option<MarionetteError>, - pub result: Value, +struct MarionetteResponse { + id: MessageId, + error: Option<MarionetteError>, + result: Value, } impl<'de> Deserialize<'de> for MarionetteResponse { @@ -1219,19 +1028,19 @@ impl MarionetteResponse { } #[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct MarionetteError { +struct MarionetteError { #[serde(rename = "error")] - pub code: String, - pub message: String, - pub stacktrace: Option<String>, + code: String, + message: String, + stacktrace: Option<String>, } -impl Into<WebDriverError> for MarionetteError { - fn into(self) -> WebDriverError { - let status = ErrorStatus::from(self.code); - let message = self.message; +impl From<MarionetteError> for WebDriverError { + fn from(error: MarionetteError) -> WebDriverError { + let status = ErrorStatus::from(error.code); + let message = error.message; - if let Some(stack) = self.stacktrace { + if let Some(stack) = error.stacktrace { WebDriverError::new_with_stack(status, message, stack) } else { WebDriverError::new(status, message) @@ -1245,25 +1054,36 @@ fn get_free_port(host: &str) -> IoResult<u16> { .map(|x| x.port()) } -pub struct MarionetteConnection { - host: String, - port: u16, - stream: Option<TcpStream>, - pub session: MarionetteSession, +struct MarionetteConnection { + browser: Browser, + session: MarionetteSession, + stream: TcpStream, } impl MarionetteConnection { - pub fn new(host: String, port: u16, session_id: Option<String>) -> MarionetteConnection { - let session = MarionetteSession::new(session_id); - MarionetteConnection { - host, - port, - stream: None, + fn new( + host: String, + port: u16, + mut browser: Browser, + session: MarionetteSession, + ) -> WebDriverResult<MarionetteConnection> { + let stream = match MarionetteConnection::connect(&host, port, &mut browser) { + Ok(stream) => stream, + Err(e) => { + if let Err(e) = browser.close(true) { + error!("Failed to stop browser: {:?}", e); + } + return Err(e); + } + }; + Ok(MarionetteConnection { + browser, session, - } + stream, + }) } - pub fn connect(&mut self, browser: &mut Option<Browser>) -> WebDriverResult<()> { + fn connect(host: &str, port: u16, browser: &mut Browser) -> WebDriverResult<TcpStream> { let timeout = time::Duration::from_secs(60); let poll_interval = time::Duration::from_millis(100); let now = time::Instant::now(); @@ -1271,52 +1091,29 @@ impl MarionetteConnection { debug!( "Waiting {}s to connect to browser on {}:{}", timeout.as_secs(), - self.host, - self.port + host, + port ); loop { // immediately abort connection attempts if process disappears - if let Some(Browser::Host(ref mut runner)) = *browser { - let exit_status = match runner.try_wait() { - Ok(Some(status)) => Some( - status - .code() - .map(|c| c.to_string()) - .unwrap_or_else(|| "signal".into()), - ), - Ok(None) => None, - Err(_) => Some("{unknown}".into()), - }; - if let Some(s) = exit_status { + if let Browser::Local(browser) = browser { + if let Some(status) = browser.check_status() { return Err(WebDriverError::new( ErrorStatus::UnknownError, - format!("Process unexpectedly closed with status {}", s), + format!("Process unexpectedly closed with status {}", status), )); } } - let try_connect = || -> WebDriverResult<(TcpStream, MarionetteHandshake)> { - let mut stream = TcpStream::connect((&self.host[..], self.port))?; - let data = MarionetteConnection::handshake(&mut stream)?; - - Ok((stream, data)) - }; - - match try_connect() { - Ok((stream, data)) => { - debug!( - "Connection to Marionette established on {}:{}.", - self.host, self.port, - ); - - self.stream = Some(stream); - self.session.application_type = Some(data.application_type); - self.session.protocol = Some(data.protocol); - break; + match MarionetteConnection::try_connect(host, port) { + Ok(stream) => { + debug!("Connection to Marionette established on {}:{}.", host, port); + return Ok(stream); } Err(e) => { if now.elapsed() < timeout { + trace!("{}. Retrying in {:?}", e.to_string(), poll_interval); thread::sleep(poll_interval); } else { return Err(WebDriverError::new(ErrorStatus::Timeout, e.to_string())); @@ -1324,17 +1121,22 @@ impl MarionetteConnection { } } } + } - Ok(()) + fn try_connect(host: &str, port: u16) -> WebDriverResult<TcpStream> { + let mut stream = TcpStream::connect((host, port))?; + MarionetteConnection::handshake(&mut stream)?; + Ok(stream) } fn handshake(stream: &mut TcpStream) -> WebDriverResult<MarionetteHandshake> { let resp = (match stream.read_timeout() { Ok(timeout) => { // If platform supports changing the read timeout of the stream, - // use a short one only for the handshake with Marionette. + // use a short one only for the handshake with Marionette. Don't + // make it shorter as 1000ms to not fail on slow connections. stream - .set_read_timeout(Some(time::Duration::from_millis(100))) + .set_read_timeout(Some(time::Duration::from_millis(1000))) .ok(); let data = MarionetteConnection::read_resp(stream); stream.set_read_timeout(timeout).ok(); @@ -1372,15 +1174,23 @@ impl MarionetteConnection { Ok(data) } - pub fn close(&self) {} + fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> { + self.stream.shutdown(Shutdown::Both)?; + self.browser.close(wait_for_shutdown)?; + Ok(()) + } - pub fn send_command( + fn send_command( &mut self, - capabilities: Option<Map<String, Value>>, msg: &WebDriverMessage<GeckoExtensionRoute>, ) -> WebDriverResult<WebDriverResponse> { let id = self.session.next_command_id(); - let enc_cmd = MarionetteCommand::from_webdriver_message(id, capabilities, msg)?; + let enc_cmd = MarionetteCommand::from_webdriver_message( + id, + &self.session.capabilities, + &self.browser, + msg, + )?; let resp_data = self.send(enc_cmd)?; let data: MarionetteResponse = serde_json::from_str(&resp_data)?; @@ -1388,30 +1198,16 @@ impl MarionetteConnection { } fn send(&mut self, data: String) -> WebDriverResult<String> { - let stream = match self.stream { - Some(ref mut stream) => { - if stream.write(&*data.as_bytes()).is_err() { - let mut err = WebDriverError::new( - ErrorStatus::UnknownError, - "Failed to write request to stream", - ); - err.delete_session = true; - return Err(err); - } - - stream - } - None => { - let mut err = WebDriverError::new( - ErrorStatus::UnknownError, - "Tried to write before opening stream", - ); - err.delete_session = true; - return Err(err); - } - }; + if self.stream.write(&*data.as_bytes()).is_err() { + let mut err = WebDriverError::new( + ErrorStatus::UnknownError, + "Failed to write request to stream", + ); + err.delete_session = true; + return Err(err); + } - match MarionetteConnection::read_resp(stream) { + match MarionetteConnection::read_resp(&mut self.stream) { Ok(resp) => Ok(resp), Err(_) => { let mut err = WebDriverError::new( @@ -1428,7 +1224,7 @@ impl MarionetteConnection { let mut bytes = 0usize; loop { - let buf = &mut [0 as u8]; + let buf = &mut [0u8]; let num_read = stream.read(buf)?; let byte = match num_read { 0 => { @@ -1437,9 +1233,9 @@ impl MarionetteConnection { "EOF reading marionette message", )) } - 1 => buf[0] as char, + 1 => buf[0], _ => panic!("Expected one byte got more"), - }; + } as char; match byte { '0'..='9' => { bytes *= 10; @@ -1450,7 +1246,7 @@ impl MarionetteConnection { } } - let buf = &mut [0 as u8; 8192]; + let buf = &mut [0u8; 8192]; let mut payload = Vec::with_capacity(bytes); let mut total_read = 0; while total_read < bytes { @@ -1628,7 +1424,7 @@ impl ToMarionette<MarionetteSelector> for LocatorStrategy { fn to_marionette(&self) -> WebDriverResult<MarionetteSelector> { use self::LocatorStrategy::*; match self { - CSSSelector => Ok(MarionetteSelector::CSS), + CSSSelector => Ok(MarionetteSelector::Css), LinkText => Ok(MarionetteSelector::LinkText), PartialLinkText => Ok(MarionetteSelector::PartialLinkText), TagName => Ok(MarionetteSelector::TagName), @@ -1662,7 +1458,7 @@ impl ToMarionette<MarionetteFrame> for SwitchToFrameParameters { fn to_marionette(&self) -> WebDriverResult<MarionetteFrame> { Ok(match &self.id { Some(x) => match x { - FrameId::Short(n) => MarionetteFrame::Index(n.clone()), + FrameId::Short(n) => MarionetteFrame::Index(*n), FrameId::Element(el) => MarionetteFrame::Element(el.0.clone()), }, None => MarionetteFrame::Parent, @@ -1715,34 +1511,3 @@ impl ToMarionette<MarionetteWindowRect> for WindowRectParameters { }) } } - -#[cfg(test)] -mod tests { - use super::{MarionetteHandler, MarionetteSettings}; - use mozprofile::preferences::PrefValue; - use mozprofile::profile::Profile; - - // This is not a pretty test, mostly due to the nature of - // mozprofile's and MarionetteHandler's APIs, but we have had - // several regressions related to marionette.log.level. - #[test] - fn test_marionette_log_level() { - let mut profile = Profile::new().unwrap(); - let handler = MarionetteHandler::new(MarionetteSettings::default()); - handler.set_prefs(2828, &mut profile, false, vec![]).ok(); - let user_prefs = profile.user_prefs().unwrap(); - - let pref = user_prefs.get("marionette.log.level").unwrap(); - let value = match pref.value { - PrefValue::String(ref s) => s, - _ => panic!(), - }; - for (i, ch) in value.chars().enumerate() { - if i == 0 { - assert!(ch.is_uppercase()); - } else { - assert!(ch.is_lowercase()); - } - } - } -} |