зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1421766 - Make geckodriver read marionette port from MarionetteActivePort in profile, r=webdriver-reviewers,jdescottes,whimboo
When no marionette port is explicitly specified, and the Firefox version is >= 95, pass in a port number of 0 from geckodriver to firefox, so that marionette binds on a free port. Then read the port it used from the profile. This avoids the possibility of races between geckodriver picking a free port and marionette binding to that port. This could also be used on Android, but isn't implemented as part of this patch. Differential Revision: https://phabricator.services.mozilla.com/D136740
This commit is contained in:
Родитель
7f74d54dd1
Коммит
f09c241a85
|
@ -1908,6 +1908,7 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
"tempfile",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webdriver",
|
"webdriver",
|
||||||
|
|
|
@ -31,5 +31,8 @@ uuid = { version = "0.8", features = ["v4"] }
|
||||||
webdriver = { path = "../webdriver" }
|
webdriver = { path = "../webdriver" }
|
||||||
zip = { version = "0.4", default-features = false, features = ["deflate"] }
|
zip = { version = "0.4", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "geckodriver"
|
name = "geckodriver"
|
||||||
|
|
|
@ -10,7 +10,8 @@ use mozprofile::preferences::Pref;
|
||||||
use mozprofile::profile::{PrefFile, Profile};
|
use mozprofile::profile::{PrefFile, Profile};
|
||||||
use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess};
|
use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::io::Read;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
|
use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
|
||||||
|
@ -22,7 +23,7 @@ pub(crate) enum Browser {
|
||||||
Remote(RemoteBrowser),
|
Remote(RemoteBrowser),
|
||||||
|
|
||||||
/// An existing browser instance not controlled by GeckoDriver
|
/// An existing browser instance not controlled by GeckoDriver
|
||||||
Existing,
|
Existing(u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Browser {
|
impl Browser {
|
||||||
|
@ -30,7 +31,15 @@ impl Browser {
|
||||||
match self {
|
match self {
|
||||||
Browser::Local(x) => x.close(wait_for_shutdown),
|
Browser::Local(x) => x.close(wait_for_shutdown),
|
||||||
Browser::Remote(x) => x.close(),
|
Browser::Remote(x) => x.close(),
|
||||||
Browser::Existing => Ok(()),
|
Browser::Existing(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn marionette_port(&mut self) -> Option<u16> {
|
||||||
|
match self {
|
||||||
|
Browser::Local(x) => x.marionette_port(),
|
||||||
|
Browser::Remote(x) => x.marionette_port(),
|
||||||
|
Browser::Existing(x) => Some(*x),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,8 +47,10 @@ impl Browser {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// A local Firefox process, running on this (host) device.
|
/// A local Firefox process, running on this (host) device.
|
||||||
pub(crate) struct LocalBrowser {
|
pub(crate) struct LocalBrowser {
|
||||||
process: FirefoxProcess,
|
marionette_port: u16,
|
||||||
prefs_backup: Option<PrefsBackup>,
|
prefs_backup: Option<PrefsBackup>,
|
||||||
|
process: FirefoxProcess,
|
||||||
|
profile_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalBrowser {
|
impl LocalBrowser {
|
||||||
|
@ -79,6 +90,7 @@ impl LocalBrowser {
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let profile_path = profile.path.clone();
|
||||||
let mut runner = FirefoxRunner::new(&binary, profile);
|
let mut runner = FirefoxRunner::new(&binary, profile);
|
||||||
|
|
||||||
runner.arg("--marionette");
|
runner.arg("--marionette");
|
||||||
|
@ -109,8 +121,10 @@ impl LocalBrowser {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(LocalBrowser {
|
Ok(LocalBrowser {
|
||||||
process,
|
marionette_port,
|
||||||
prefs_backup,
|
prefs_backup,
|
||||||
|
process,
|
||||||
|
profile_path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +146,17 @@ impl LocalBrowser {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn marionette_port(&mut self) -> Option<u16> {
|
||||||
|
if self.marionette_port != 0 {
|
||||||
|
return Some(self.marionette_port);
|
||||||
|
}
|
||||||
|
let port = read_marionette_port(&self.profile_path);
|
||||||
|
if let Some(port) = port {
|
||||||
|
self.marionette_port = port;
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn check_status(&mut self) -> Option<String> {
|
pub(crate) fn check_status(&mut self) -> Option<String> {
|
||||||
match self.process.try_wait() {
|
match self.process.try_wait() {
|
||||||
Ok(Some(status)) => Some(
|
Ok(Some(status)) => Some(
|
||||||
|
@ -146,10 +171,33 @@ impl LocalBrowser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_marionette_port(profile_path: &Path) -> Option<u16> {
|
||||||
|
let port_file = profile_path.join("MarionetteActivePort");
|
||||||
|
let mut port_str = String::with_capacity(6);
|
||||||
|
let mut file = match fs::File::open(&port_file) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => {
|
||||||
|
trace!("Failed to open {}", &port_file.to_string_lossy());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = file.read_to_string(&mut port_str) {
|
||||||
|
trace!("Failed to read {}: {}", &port_file.to_string_lossy(), e);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
println!("Read port: {}", port_str);
|
||||||
|
let port = port_str.parse::<u16>().ok();
|
||||||
|
if port.is_none() {
|
||||||
|
warn!("Failed fo convert {} to u16", &port_str);
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// A remote instance, running on a (target) Android device.
|
/// A remote instance, running on a (target) Android device.
|
||||||
pub(crate) struct RemoteBrowser {
|
pub(crate) struct RemoteBrowser {
|
||||||
handler: AndroidHandler,
|
handler: AndroidHandler,
|
||||||
|
marionette_port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RemoteBrowser {
|
impl RemoteBrowser {
|
||||||
|
@ -185,13 +233,20 @@ impl RemoteBrowser {
|
||||||
|
|
||||||
handler.launch()?;
|
handler.launch()?;
|
||||||
|
|
||||||
Ok(RemoteBrowser { handler })
|
Ok(RemoteBrowser {
|
||||||
|
handler,
|
||||||
|
marionette_port,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(self) -> WebDriverResult<()> {
|
fn close(self) -> WebDriverResult<()> {
|
||||||
self.handler.force_stop()?;
|
self.handler.force_stop()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn marionette_port(&mut self) -> Option<u16> {
|
||||||
|
Some(self.marionette_port)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_prefs(
|
fn set_prefs(
|
||||||
|
@ -284,12 +339,15 @@ impl PrefsBackup {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::set_prefs;
|
use super::set_prefs;
|
||||||
|
use crate::browser::read_marionette_port;
|
||||||
use crate::capabilities::FirefoxOptions;
|
use crate::capabilities::FirefoxOptions;
|
||||||
use mozprofile::preferences::{Pref, PrefValue};
|
use mozprofile::preferences::{Pref, PrefValue};
|
||||||
use mozprofile::profile::Profile;
|
use mozprofile::profile::Profile;
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn example_profile() -> Value {
|
fn example_profile() -> Value {
|
||||||
let mut profile_data = Vec::with_capacity(1024);
|
let mut profile_data = Vec::with_capacity(1024);
|
||||||
|
@ -420,4 +478,24 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(final_prefs_data, initial_prefs_data);
|
assert_eq!(final_prefs_data, initial_prefs_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_local_marionette_port() {
|
||||||
|
fn create_port_file(profile_path: &Path, data: &[u8]) {
|
||||||
|
let port_path = profile_path.join("MarionetteActivePort");
|
||||||
|
let mut file = File::create(&port_path).unwrap();
|
||||||
|
file.write_all(data).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let profile_dir = tempdir().unwrap();
|
||||||
|
let profile_path = profile_dir.path();
|
||||||
|
assert_eq!(read_marionette_port(&profile_path), None);
|
||||||
|
assert_eq!(read_marionette_port(&profile_path), None);
|
||||||
|
create_port_file(&profile_path, b"");
|
||||||
|
assert_eq!(read_marionette_port(&profile_path), None);
|
||||||
|
create_port_file(&profile_path, b"1234");
|
||||||
|
assert_eq!(read_marionette_port(&profile_path), Some(1234));
|
||||||
|
create_port_file(&profile_path, b"1234abc");
|
||||||
|
assert_eq!(read_marionette_port(&profile_path), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time;
|
use std::time;
|
||||||
|
use webdriver::capabilities::BrowserCapabilities;
|
||||||
use webdriver::command::WebDriverCommand::{
|
use webdriver::command::WebDriverCommand::{
|
||||||
AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert,
|
AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert,
|
||||||
ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension,
|
ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension,
|
||||||
|
@ -110,8 +111,8 @@ impl MarionetteHandler {
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
new_session_parameters: &NewSessionParameters,
|
new_session_parameters: &NewSessionParameters,
|
||||||
) -> WebDriverResult<MarionetteConnection> {
|
) -> WebDriverResult<MarionetteConnection> {
|
||||||
let (capabilities, options) = {
|
|
||||||
let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref());
|
let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref());
|
||||||
|
let (capabilities, options) = {
|
||||||
let mut capabilities = new_session_parameters
|
let mut capabilities = new_session_parameters
|
||||||
.match_browser(&mut fx_capabilities)?
|
.match_browser(&mut fx_capabilities)?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
|
@ -122,7 +123,7 @@ impl MarionetteHandler {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let options = FirefoxOptions::from_capabilities(
|
let options = FirefoxOptions::from_capabilities(
|
||||||
fx_capabilities.chosen_binary,
|
fx_capabilities.chosen_binary.clone(),
|
||||||
&self.settings,
|
&self.settings,
|
||||||
&mut capabilities,
|
&mut capabilities,
|
||||||
)?;
|
)?;
|
||||||
|
@ -134,14 +135,38 @@ impl MarionetteHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
let marionette_host = self.settings.host.to_owned();
|
let marionette_host = self.settings.host.to_owned();
|
||||||
let marionette_port = self
|
let marionette_port = match self.settings.port {
|
||||||
.settings
|
Some(port) => port,
|
||||||
.port
|
None => {
|
||||||
.unwrap_or(get_free_port(&marionette_host)?);
|
// If we're launching Firefox Desktop version 95 or later, and there's no port
|
||||||
|
// specified, we can pass 0 as the port and later read it back from
|
||||||
|
// the profile.
|
||||||
|
let can_use_profile: bool = options.android.is_none()
|
||||||
|
&& !self.settings.connect_existing
|
||||||
|
&& fx_capabilities
|
||||||
|
.browser_version(&capabilities)
|
||||||
|
.map(|opt_v| {
|
||||||
|
opt_v
|
||||||
|
.map(|v| {
|
||||||
|
fx_capabilities
|
||||||
|
.compare_browser_version(&v, ">=95")
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
if can_use_profile {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
get_free_port(&marionette_host)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let websocket_port = match options.use_websocket {
|
let websocket_port = if options.use_websocket {
|
||||||
true => Some(self.settings.websocket_port),
|
Some(self.settings.websocket_port)
|
||||||
false => None,
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let browser = if options.android.is_some() {
|
let browser = if options.android.is_some() {
|
||||||
|
@ -167,10 +192,10 @@ impl MarionetteHandler {
|
||||||
self.settings.jsdebugger,
|
self.settings.jsdebugger,
|
||||||
)?)
|
)?)
|
||||||
} else {
|
} else {
|
||||||
Browser::Existing
|
Browser::Existing(marionette_port)
|
||||||
};
|
};
|
||||||
let session = MarionetteSession::new(session_id, capabilities);
|
let session = MarionetteSession::new(session_id, capabilities);
|
||||||
MarionetteConnection::new(marionette_host, marionette_port, browser, session)
|
MarionetteConnection::new(marionette_host, browser, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close_connection(&mut self, wait_for_shutdown: bool) {
|
fn close_connection(&mut self, wait_for_shutdown: bool) {
|
||||||
|
@ -740,7 +765,7 @@ fn try_convert_to_marionette_message(
|
||||||
flags: vec![AppStatus::eForceQuit],
|
flags: vec![AppStatus::eForceQuit],
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
Browser::Existing => Some(Command::WebDriver(
|
Browser::Existing(_) => Some(Command::WebDriver(
|
||||||
MarionetteWebDriverCommand::DeleteSession,
|
MarionetteWebDriverCommand::DeleteSession,
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
|
@ -1101,11 +1126,10 @@ struct MarionetteConnection {
|
||||||
impl MarionetteConnection {
|
impl MarionetteConnection {
|
||||||
fn new(
|
fn new(
|
||||||
host: String,
|
host: String,
|
||||||
port: u16,
|
|
||||||
mut browser: Browser,
|
mut browser: Browser,
|
||||||
session: MarionetteSession,
|
session: MarionetteSession,
|
||||||
) -> WebDriverResult<MarionetteConnection> {
|
) -> WebDriverResult<MarionetteConnection> {
|
||||||
let stream = match MarionetteConnection::connect(&host, port, &mut browser) {
|
let stream = match MarionetteConnection::connect(&host, &mut browser) {
|
||||||
Ok(stream) => stream,
|
Ok(stream) => stream,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Err(e) = browser.close(true) {
|
if let Err(e) = browser.close(true) {
|
||||||
|
@ -1121,16 +1145,15 @@ impl MarionetteConnection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect(host: &str, port: u16, browser: &mut Browser) -> WebDriverResult<TcpStream> {
|
fn connect(host: &str, browser: &mut Browser) -> WebDriverResult<TcpStream> {
|
||||||
let timeout = time::Duration::from_secs(60);
|
let timeout = time::Duration::from_secs(60);
|
||||||
let poll_interval = time::Duration::from_millis(100);
|
let poll_interval = time::Duration::from_millis(100);
|
||||||
let now = time::Instant::now();
|
let now = time::Instant::now();
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Waiting {}s to connect to browser on {}:{}",
|
"Waiting {}s to connect to browser on {}",
|
||||||
timeout.as_secs(),
|
timeout.as_secs(),
|
||||||
host,
|
host,
|
||||||
port
|
|
||||||
);
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -1144,19 +1167,30 @@ impl MarionetteConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let last_err;
|
||||||
|
|
||||||
|
if let Some(port) = browser.marionette_port() {
|
||||||
match MarionetteConnection::try_connect(host, port) {
|
match MarionetteConnection::try_connect(host, port) {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
debug!("Connection to Marionette established on {}:{}.", host, port);
|
debug!("Connection to Marionette established on {}:{}.", host, port);
|
||||||
return Ok(stream);
|
return Ok(stream);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
let err_str = e.to_string();
|
||||||
|
last_err = Some(err_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
last_err = Some("Failed to read marionette port".into());
|
||||||
|
}
|
||||||
if now.elapsed() < timeout {
|
if now.elapsed() < timeout {
|
||||||
trace!("{}. Retrying in {:?}", e.to_string(), poll_interval);
|
trace!("Retrying in {:?}", poll_interval);
|
||||||
thread::sleep(poll_interval);
|
thread::sleep(poll_interval);
|
||||||
} else {
|
} else {
|
||||||
return Err(WebDriverError::new(ErrorStatus::Timeout, e.to_string()));
|
return Err(WebDriverError::new(
|
||||||
}
|
ErrorStatus::Timeout,
|
||||||
}
|
last_err.unwrap_or_else(|| "Unknown error".into()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче